@surething/cockpit 1.0.259 → 1.0.261
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-prod/BUILD_ID +1 -1
- package/.next-prod/app-path-routes-manifest.json +1 -1
- package/.next-prod/build-manifest.json +2 -2
- package/.next-prod/cockpit-build.json +1 -1
- package/.next-prod/prerender-manifest.json +4 -4
- package/.next-prod/server/app/_global-error/page.js.nft.json +1 -1
- package/.next-prod/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/_global-error.html +1 -1
- package/.next-prod/server/app/_global-error.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next-prod/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next-prod/server/app/_not-found/page.js +1 -1
- package/.next-prod/server/app/_not-found/page.js.nft.json +1 -1
- package/.next-prod/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/_not-found.html +2 -2
- package/.next-prod/server/app/_not-found.rsc +3 -3
- package/.next-prod/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next-prod/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/.next-prod/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next-prod/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next-prod/server/app/api/agent/test/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/bash/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/codex/route.js +1 -1
- package/.next-prod/server/app/api/chat/codex/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/deepseek/route.js +1 -1
- package/.next-prod/server/app/api/chat/deepseek/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/glm/route.js +1 -1
- package/.next-prod/server/app/api/chat/glm/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/kimi/route.js +1 -1
- package/.next-prod/server/app/api/chat/kimi/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/ollama/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/route.js +1 -1
- package/.next-prod/server/app/api/chat/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/chat/stop/route.js +1 -1
- package/.next-prod/server/app/api/chat/stop/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/claude-stats/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/commands/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/comments/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/columns/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/export/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/query/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/db/schemas/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/deepseek/balance/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/deepseek/credentials/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/deepseek/models/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/dev/spans/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/extension/version/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/file/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/blame/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/clipboard/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/copy/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/delete/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/expanded/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/index/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/init/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/paste/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/read/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/readdir/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/recent/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/save/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/search/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/stat/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/files/text/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/branch-diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/branches/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/commit-diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/commits/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/current-branch/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/discard/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/stage/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/status/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/unstage/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/git/worktree/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/glm/credentials/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/glm/models/route.js +1 -1
- package/.next-prod/server/app/api/glm/models/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/glm/usage/route.js +1 -1
- package/.next-prod/server/app/api/glm/usage/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/global-state/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/health/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/html-apps/[id]/route.js +1 -1
- package/.next-prod/server/app/api/html-apps/[id]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/html-apps/route.js +1 -1
- package/.next-prod/server/app/api/html-apps/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/jupyter/load/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/jupyter/save/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/jupyter/shutdown/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/kimi/credentials/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/kimi/models/route.js +1 -1
- package/.next-prod/server/app/api/kimi/models/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/kimi/usage/route.js +1 -1
- package/.next-prod/server/app/api/kimi/usage/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/definition/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/hover/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/references/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/status/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/lsp/warmup/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/columns/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/export/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/query/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/mysql/schemas/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/query/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/neo4j/schema/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/note/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/ollama/config/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/ollama/models/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/ollama/start/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/open-cursor/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/open-vscode/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/pick-folder/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/pinned-sessions/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/project-settings/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/project-state/route.js +1 -1
- package/.next-prod/server/app/api/project-state/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/affected/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/callees/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/callers/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/coedit/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/context/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/file/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/file-functions/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/impact/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/related/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/risk/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projectGraph/search/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/projects/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/prompts/config/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/push/public-key/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/push/subscribe/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/push/unsubscribe/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/command/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/connect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/delete/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/disconnect/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/get/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/keys/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/redis/set/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/[id]/comments/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/[id]/replies/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/[id]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/identify/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/order/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/share-info/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/review/users/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/scheduled-tasks/route.js +2 -2
- package/.next-prod/server/app/api/scheduled-tasks/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/services/config/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/services/scripts/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/session/[sessionId]/fork/route.js +1 -1
- package/.next-prod/server/app/api/session/[sessionId]/fork/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/session/[sessionId]/history/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/session-by-path/route.js +1 -1
- package/.next-prod/server/app/api/session-by-path/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/sessions/projects/[encodedPath]/route.js +1 -1
- package/.next-prod/server/app/api/sessions/projects/[encodedPath]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/sessions/projects/route.js +1 -1
- package/.next-prod/server/app/api/sessions/projects/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/settings/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/skills/[id]/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/skills/content/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/skills/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/snapshots/diff/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/snapshots/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/aliases/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/autocomplete/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/bubble-order/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/env/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/terminal/history/route.js.nft.json +1 -1
- package/.next-prod/server/app/api/version/route.js.nft.json +1 -1
- package/.next-prod/server/app/apps/[...path]/route.js +1 -1
- package/.next-prod/server/app/apps/[...path]/route.js.nft.json +1 -1
- package/.next-prod/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next-prod/server/app/m/page.js +1 -1
- package/.next-prod/server/app/m/page.js.nft.json +1 -1
- package/.next-prod/server/app/m/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next-prod/server/app/page.js +1 -1
- package/.next-prod/server/app/page.js.nft.json +1 -1
- package/.next-prod/server/app/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/project/page.js +1 -1
- package/.next-prod/server/app/project/page.js.nft.json +1 -1
- package/.next-prod/server/app/project/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app/review/[id]/page.js +1 -1
- package/.next-prod/server/app/review/[id]/page.js.nft.json +1 -1
- package/.next-prod/server/app/review/[id]/page_client-reference-manifest.js +1 -1
- package/.next-prod/server/app-paths-manifest.json +1 -1
- package/.next-prod/server/chunks/3286.js +406 -270
- package/.next-prod/server/chunks/{9004.js → 3641.js} +2 -2
- package/.next-prod/server/chunks/516.js +1 -0
- package/.next-prod/server/chunks/5750.js +16 -10
- package/.next-prod/server/chunks/6495.js +1 -1
- package/.next-prod/server/chunks/6548.js +1 -1
- package/.next-prod/server/chunks/816.js +2 -0
- package/.next-prod/server/chunks/841.js +12 -10
- package/.next-prod/server/chunks/9352.js +4 -4
- package/.next-prod/server/chunks/9830.js +1 -1
- package/.next-prod/server/middleware-build-manifest.js +1 -1
- package/.next-prod/server/pages/404.html +2 -2
- package/.next-prod/server/pages/500.html +1 -1
- package/.next-prod/server/server-reference-manifest.json +1 -1
- package/.next-prod/static/chunks/1157-164bcf046a0a9b15.js +32 -0
- package/.next-prod/static/chunks/1372-d16d68b1b03a345a.js +15 -0
- package/.next-prod/static/chunks/{2653-3f8bfa49727caa72.js → 4333-9e6709e96d3ec973.js} +7 -7
- package/.next-prod/static/chunks/{225-75b2e22f496b56a1.js → 6160-958532439d324185.js} +2 -2
- package/.next-prod/static/chunks/6784-09dffdb0e8ca7fb3.js +381 -0
- package/.next-prod/static/chunks/app/{layout-fef0d44c94fcb415.js → layout-ab52fc1a83af968b.js} +1 -1
- package/.next-prod/static/chunks/app/m/page-7dcf56a65528216f.js +1 -0
- package/.next-prod/static/chunks/app/{page-824be00a023f34d7.js → page-5de8b6584e328a6f.js} +1 -1
- package/.next-prod/static/chunks/app/project/{page-824be00a023f34d7.js → page-5de8b6584e328a6f.js} +1 -1
- package/.next-prod/static/css/83c134c954630942.css +1 -0
- package/.next-prod/trace +14 -14
- package/.next-prod/trace-build +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/apps/devkit/app.jsx +730 -0
- package/apps/devkit/index.html +178 -0
- package/apps/devkit/md5.js +98 -0
- package/dist/{chunk-L5TWVHYB.mjs → chunk-B5LXMESP.mjs} +19 -9
- package/dist/{chunk-YEZ3U3F2.mjs → chunk-CLPJYIFC.mjs} +1 -1
- package/dist/{chunk-RSRPFWNL.mjs → chunk-TU3GL2HK.mjs} +1 -27
- package/dist/httpApi.mjs +2 -2
- package/dist/scheduledTasks.mjs +685 -462
- package/dist/{server-UZPDELUK.mjs → server-2UJDVZ32.mjs} +2 -2
- package/dist/wsServer.mjs +5 -4
- package/package.json +1 -1
- package/public/agent-icons/claude.svg +1 -0
- package/public/agent-icons/codex.svg +1 -0
- package/public/agent-icons/deepseek.svg +1 -0
- package/public/agent-icons/glm.svg +1 -0
- package/public/agent-icons/kimi.svg +1 -0
- package/public/agent-icons/ollama.svg +1 -0
- package/.next-prod/server/chunks/151.js +0 -1
- package/.next-prod/static/chunks/1372-d94b8f9ed5cd2713.js +0 -15
- package/.next-prod/static/chunks/6784-ec12c61b77c64dc0.js +0 -381
- package/.next-prod/static/chunks/7482-3ffe7da02d3e7f45.js +0 -26
- package/.next-prod/static/chunks/app/m/page-aff83d3f5f4047c5.js +0 -1
- package/.next-prod/static/css/a82580b2376d7cae.css +0 -1
- /package/.next-prod/static/{F9dxCcbBY_bNPY4fxWaWe → pgteRwI6FQcp683smeSNO}/_buildManifest.js +0 -0
- /package/.next-prod/static/{F9dxCcbBY_bNPY4fxWaWe → pgteRwI6FQcp683smeSNO}/_ssgManifest.js +0 -0
|
@@ -459,348 +459,484 @@ Finally emit [Overall recap: end-to-end interaction verification + total deliver
|
|
|
459
459
|
- **Recap over interruption**: one final review > mid-flow breaks
|
|
460
460
|
`},html:{zh:`---
|
|
461
461
|
name: html
|
|
462
|
-
description: "
|
|
463
|
-
argument-hint: "
|
|
462
|
+
description: "生成可交互的本地 React 应用:内置 cockpit bash SDK,执行命令取/改数据"
|
|
463
|
+
argument-hint: "描述你想要的应用"
|
|
464
464
|
---
|
|
465
465
|
|
|
466
|
-
# 生成 Cockpit
|
|
466
|
+
# 生成 Cockpit 应用(本地 React 页面)
|
|
467
467
|
|
|
468
|
-
|
|
469
|
-
|
|
468
|
+
一个**本地应用**,不是一次性页面:预览时注入了全局 \`window.cockpit\` SDK —— 本质就是 Bash 工具
|
|
469
|
+
暴露给页面,按钮能 \`curl\`、读写文件、跑脚本;数据落盘在 app 目录里,重开即用。
|
|
470
470
|
|
|
471
|
-
|
|
472
|
-
表单);只有**极简单的单视图、或纯静态一次性页**才退回单文件内联 HTML。两种模式共用下面
|
|
473
|
-
这套契约。
|
|
471
|
+
零构建:React / Babel 从同源的 \`/html-lib\` 加载,不打包、不装依赖、离线可用。
|
|
474
472
|
|
|
475
|
-
##
|
|
473
|
+
## 一、流程:先对齐,确认后再写(重要)
|
|
476
474
|
|
|
477
475
|
**别一上来就写文件**,分三步:
|
|
478
476
|
|
|
479
|
-
1. **对齐需求** ——
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
477
|
+
1. **对齐需求** —— 先复述你对这个应用的理解(做什么、数据从哪来、关键功能与交互)。仅当需求有
|
|
478
|
+
歧义或存在多种合理解读时才回问;已经清楚就直接给一段简短的理解摘要,不必逐条追问。
|
|
479
|
+
2. **告知存放目录** —— 定出 \`<目录>/<name>/\`,明确告诉用户准备存哪、叫什么名;用户可否决或改:
|
|
480
|
+
- \`<name>\` **一律由你按需求起名**(短横线小写,与 \`cockpit-name\` 一致),用户只描述需求、
|
|
481
|
+
不负责起名。
|
|
482
|
+
- 用户在需求里给了目录 → \`<用户给的目录>/<name>/\`;没给 → **当前聊天工作目录**(本次会话的
|
|
483
|
+
cwd)下的 \`<name>/\`。
|
|
484
|
+
- 一个 app 的所有文件都在这个目录里,数据按第四节分到 \`cache/\` \`state/\` \`out/\`。别把文件
|
|
485
|
+
散落到别处,也别自造 \`.cockpit-apps\` 之类目录。
|
|
486
|
+
3. **确认后才写** —— 等用户明确同意(确认 / 开始 / 写吧 / go 等任意肯定表示)再 \`Write\` 落盘。
|
|
487
|
+
确认之前只讨论、不写文件。
|
|
486
488
|
|
|
487
|
-
即便用户在 \`/html\` 后已经把需求和目录都写清楚了,也要先停一轮,给出「理解 + 目录 +
|
|
488
|
-
|
|
489
|
+
即便用户在 \`/html\` 后已经把需求和目录都写清楚了,也要先停一轮,给出「理解 + 目录 + 一句『确认
|
|
490
|
+
就开始写?』」等待确认——但这一轮要**轻量**,别反复追问。
|
|
489
491
|
|
|
490
|
-
##
|
|
492
|
+
## 二、骨架:三个文件,照抄
|
|
491
493
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
- \`cockpit.cwd: string\` —— 当前文件所在目录,相对路径命令默认在此执行。
|
|
495
|
-
- \`cockpit.bash(command, opts?)\` —— 执行一条 bash 命令(\`command\` 是整条 shell 串):
|
|
496
|
-
- **前台**(默认,短/离散命令)→ \`Promise<{ stdout, stderr, exitCode }>\`,\`await\` 后一次性拿全部输出。
|
|
497
|
-
- **后台**(\`opts.background: true\`,长/实时命令)→ \`{ kill() }\`,经回调流式输出:
|
|
498
|
-
\`opts = { background:true, cwd?, onOutput?, onStderr?, onExit?, onError? }\`。大/持续输出必须用后台(前台全缓存在内存)。
|
|
499
|
-
|
|
500
|
-
写这类页面必须守的几条:
|
|
501
|
-
|
|
502
|
-
1. **数据只走 \`cockpit.bash\`**(\`curl\` 取、\`curl -X POST\` 改、或读写文件),**绝不用
|
|
503
|
-
\`fetch(外部URL)\`** —— 同源沙箱会 CORS 拦截。
|
|
504
|
-
2. **两类失败都要处理**(前台):\`try/catch\` 抓到的**抛异常 = spawn/基建失败**(坏路径、
|
|
505
|
-
连接断);没抛但 **\`exitCode !== 0\` = 命令跑了但失败**,展示 \`stderr\` 或 stdout 里的错误
|
|
506
|
-
体,别只写"失败"。后台对应 \`onError\`(基建)+ \`onExit\` 非零 \`code\`(命令)。
|
|
507
|
-
3. **\`command\` 是 shell 串**:动态值/用户输入拼进去前必须校验或转义(shell 元字符会注入);
|
|
508
|
-
能用固定命令就别裸拼。
|
|
509
|
-
4. **资源引用**:同级相对(\`./app.jsx\`、\`./style.css\`、图片)和 CDN 绝对 URL 都能加载;根
|
|
510
|
-
相对(\`/assets/x.css\`)会 404。
|
|
511
|
-
|
|
512
|
-
meta 头(务必写上,供 HTML 面板做卡片、console \`/name\` 打开):
|
|
494
|
+
\`index.html\` —— 外壳固定这样写,别自由发挥:
|
|
513
495
|
|
|
514
496
|
\`\`\`html
|
|
515
|
-
|
|
516
|
-
<
|
|
517
|
-
<
|
|
518
|
-
<meta
|
|
519
|
-
<
|
|
497
|
+
<!doctype html>
|
|
498
|
+
<html>
|
|
499
|
+
<head>
|
|
500
|
+
<meta charset="utf-8">
|
|
501
|
+
<title>页面标题</title>
|
|
502
|
+
<meta name="cockpit-name" content="short-name"> <!-- /name 短名,唯一、只用字母数字-_ -->
|
|
503
|
+
<meta name="description" content="一句话说明">
|
|
504
|
+
<meta name="cockpit-icon" content="🔍"> <!-- emoji 或图标 url,可选 -->
|
|
505
|
+
<meta name="cockpit-theme" content="auto"> <!-- 亮/暗切换,见第五节 -->
|
|
506
|
+
<link rel="stylesheet" href="/html-lib/theme.css">
|
|
507
|
+
</head>
|
|
508
|
+
<body>
|
|
509
|
+
<div id="root"></div>
|
|
510
|
+
<script src="/html-lib/react.production.min.js"></script>
|
|
511
|
+
<script src="/html-lib/react-dom.production.min.js"></script>
|
|
512
|
+
<script src="/html-lib/babel.min.js"></script>
|
|
513
|
+
<script>
|
|
514
|
+
(async () => {
|
|
515
|
+
const src = await (await fetch('./app.jsx')).text();
|
|
516
|
+
// 必须 classic runtime:默认 automatic 会注入 import react/jsx-runtime,
|
|
517
|
+
// 作为普通脚本注入会报 "Cannot use import statement outside a module"
|
|
518
|
+
const { code } = Babel.transform(src, { presets: [['react', { runtime: 'classic' }]] });
|
|
519
|
+
const s = document.createElement('script'); s.textContent = code; document.body.appendChild(s);
|
|
520
|
+
})();
|
|
521
|
+
</script>
|
|
522
|
+
</body>
|
|
523
|
+
</html>
|
|
520
524
|
\`\`\`
|
|
521
525
|
|
|
522
|
-
|
|
526
|
+
meta 头四行都要写:HTML 面板靠它做卡片,console 靠 \`cockpit-name\` 用 \`/name\` 打开。
|
|
523
527
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
\`\`\`html
|
|
527
|
-
<div id="root"></div>
|
|
528
|
-
<script src="/html-lib/react.production.min.js"></script>
|
|
529
|
-
<script src="/html-lib/react-dom.production.min.js"></script>
|
|
530
|
-
<script src="/html-lib/babel.min.js"></script>
|
|
531
|
-
<script>
|
|
532
|
-
(async () => {
|
|
533
|
-
const src = await (await fetch('./app.jsx')).text();
|
|
534
|
-
// 必须 classic runtime:默认 automatic 会注入 import react/jsx-runtime,
|
|
535
|
-
// 作为普通脚本注入会报 "Cannot use import statement outside a module"
|
|
536
|
-
const { code } = Babel.transform(src, { presets: [['react', { runtime: 'classic' }]] });
|
|
537
|
-
const s = document.createElement('script'); s.textContent = code; document.body.appendChild(s);
|
|
538
|
-
})();
|
|
539
|
-
</script>
|
|
540
|
-
\`\`\`
|
|
541
|
-
|
|
542
|
-
- \`/html-lib\` 是 Cockpit 本地托管(同源、离线可用,无需 CDN)。
|
|
543
|
-
- \`app.jsx\` 用全局 \`React\`(\`const { useState } = React;\`),末尾
|
|
544
|
-
\`ReactDOM.createRoot(document.getElementById('root')).render(<App/>)\`。
|
|
545
|
-
- 样式默认套 **Cockpit 主题**:\`<link rel="stylesheet" href="/html-lib/theme.css">\`,直接用
|
|
546
|
-
语义变量(\`var(--background)\` / \`--foreground\` / \`--card\` / \`--brand\` / \`--muted-foreground\` /
|
|
547
|
-
\`--border\` / \`--destructive\`;半透明用 \`hsl(var(--green-9) / .12)\`)。上面 meta 头里的
|
|
548
|
-
\`cockpit-theme\` 让预览带上一个浮动亮/暗切换按钮(默认右上角,用户可拖动吸附到任意一角,
|
|
549
|
-
所以不必为它在右上角留空位);**默认跟随 Cockpit 宿主主题**(Cockpit 外打开时按
|
|
550
|
-
meta 的 auto/light/dark),手动切换后**按 app 记住选择(跨刷新,优先于跟随)**,也可调
|
|
551
|
-
\`cockpit.toggleTheme()\`。**别自拍调色板、别上 Tailwind**;app 特有细节再补一小段 \`<style>\`。
|
|
552
|
-
|
|
553
|
-
React 里取数同样走 \`cockpit.bash\`:
|
|
528
|
+
\`app.jsx\` —— 全局 \`React\`,末尾 render。**打开只读缓存,点刷新才回源**:
|
|
554
529
|
|
|
555
530
|
\`\`\`jsx
|
|
531
|
+
const { useState, useEffect, useCallback } = React;
|
|
532
|
+
|
|
556
533
|
function App() {
|
|
557
|
-
const [
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
534
|
+
const [data, setData] = useState(null);
|
|
535
|
+
const [busy, setBusy] = useState(false);
|
|
536
|
+
const [err, setErr] = useState(null);
|
|
537
|
+
|
|
538
|
+
const load = useCallback(async (refresh) => {
|
|
539
|
+
setBusy(true); setErr(null);
|
|
540
|
+
try {
|
|
541
|
+
const arg = btoa(JSON.stringify({ refresh: !!refresh })); // 结构化入参走 base64
|
|
542
|
+
const { stdout, stderr, exitCode } = await cockpit.bash(\`node ./api.mjs \${arg}\`);
|
|
543
|
+
if (exitCode !== 0) throw new Error(stderr || '脚本失败'); // 命令跑了但失败
|
|
544
|
+
const d = JSON.parse(stdout);
|
|
545
|
+
if (d.error && !d.stale) throw new Error(d.error);
|
|
546
|
+
setData(d);
|
|
547
|
+
if (d.stale) setErr(d.error); // 有旧数据可显示
|
|
548
|
+
} catch (e) {
|
|
549
|
+
setErr(e.message); // 抛异常 = spawn/基建失败。已有 data 就留着,别清成错误页
|
|
550
|
+
} finally { setBusy(false); }
|
|
551
|
+
}, []);
|
|
552
|
+
|
|
553
|
+
useEffect(() => { load(false); }, [load]); // 打开:只读缓存,0 次回源
|
|
554
|
+
|
|
555
|
+
if (!data) return <div>{err ?? '加载中…'}</div>;
|
|
556
|
+
return (
|
|
557
|
+
<div>
|
|
558
|
+
<span>{data.cached ? '缓存' : '已更新'} \xb7 {new Date(data.fetchedAt).toLocaleString('zh-CN')}</span>
|
|
559
|
+
<button disabled={busy} onClick={() => load(true)}>{busy ? '刷新中…' : '刷新'}</button>
|
|
560
|
+
{err && <em>刷新失败,下面是旧数据:{err}</em>}
|
|
561
|
+
{/* 渲染 data */}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
563
564
|
}
|
|
564
|
-
\`\`\`
|
|
565
565
|
|
|
566
|
-
|
|
566
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
567
|
+
\`\`\`
|
|
567
568
|
|
|
568
|
-
|
|
569
|
-
写成同目录**脚本文件**,\`cockpit.bash\` 调它——HTML/JSX 管展示,脚本当后端处理器。
|
|
569
|
+
\`api.mjs\` —— 后端处理器:取数、解析、落盘都在这里,stdout 吐一个 JSON:
|
|
570
570
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
- 脚本用 stdout 返回 JSON,前端 \`JSON.parse\`。先 \`Write\` 出脚本,再在页面调它。
|
|
571
|
+
\`\`\`js
|
|
572
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
573
|
+
import { join, dirname } from 'node:path';
|
|
574
|
+
import { fileURLToPath } from 'node:url';
|
|
576
575
|
|
|
577
|
-
|
|
576
|
+
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
577
|
+
const CACHE = join(DIR, 'cache/data.json');
|
|
578
|
+
const VERSION = 1; // payload 结构一改就 bump,否则老缓存喂给新前端
|
|
578
579
|
|
|
579
|
-
|
|
580
|
+
const args = JSON.parse(Buffer.from(process.argv[2] || '', 'base64').toString() || '{}');
|
|
580
581
|
|
|
581
|
-
|
|
582
|
+
function readCache() {
|
|
583
|
+
if (!existsSync(CACHE)) return null;
|
|
584
|
+
const c = JSON.parse(readFileSync(CACHE, 'utf8'));
|
|
585
|
+
return c.v === VERSION ? c : null; // 版本不符 = 作废,当没有
|
|
586
|
+
}
|
|
587
|
+
function writeCache(payload) {
|
|
588
|
+
mkdirSync(dirname(CACHE), { recursive: true });
|
|
589
|
+
const tmp = \`\${CACHE}.\${process.pid}.tmp\`; // 原子写:中途被杀不会留下半个 JSON
|
|
590
|
+
writeFileSync(tmp, JSON.stringify({ v: VERSION, fetchedAt: new Date().toISOString(), payload }));
|
|
591
|
+
renameSync(tmp, CACHE);
|
|
592
|
+
}
|
|
582
593
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
594
|
+
try {
|
|
595
|
+
const hit = args.refresh ? null : readCache();
|
|
596
|
+
if (hit) {
|
|
597
|
+
process.stdout.write(JSON.stringify({ ...hit.payload, cached: true, fetchedAt: hit.fetchedAt }));
|
|
598
|
+
} else {
|
|
599
|
+
const payload = await fetchFresh(args); // ← 你的取数逻辑(curl / 查库 / 跑命令)
|
|
600
|
+
writeCache(payload);
|
|
601
|
+
process.stdout.write(JSON.stringify({ ...payload, cached: false, fetchedAt: new Date().toISOString() }));
|
|
602
|
+
}
|
|
603
|
+
} catch (e) {
|
|
604
|
+
const hit = readCache(); // 刷新失败:回落旧缓存并标注,别让页面空掉
|
|
605
|
+
process.stdout.write(JSON.stringify(hit
|
|
606
|
+
? { ...hit.payload, cached: true, stale: true, error: e.message, fetchedAt: hit.fetchedAt }
|
|
607
|
+
: { error: e.message }));
|
|
608
|
+
}
|
|
597
609
|
\`\`\`
|
|
598
610
|
|
|
599
|
-
|
|
611
|
+
- 一条命令就够、数据也不值得留(纯查看当前状态)时,\`api.mjs\` 可以省掉,直接
|
|
612
|
+
\`cockpit.bash("curl -s …")\`;但只要数据要留下来,就用这个骨架,别自己发明一套。
|
|
613
|
+
- 换语言同理:\`python3 ./api.py\` / \`bash ./api.sh\`(显式解释器,免 \`chmod +x\`);\`node\` /
|
|
614
|
+
\`python3\` 拿不准就先 \`command -v\` 探测,退回 shell。
|
|
615
|
+
- 缓存 key 要含**影响结果的参数**(时间范围、抓取深度…):把 \`cache/data.json\` 换成
|
|
616
|
+
\`cache/<param>.json\` 或在文件里按 key 分桶,否则会拿 7 天的缓存去渲染 30 天的视图。
|
|
600
617
|
|
|
601
|
-
|
|
602
|
-
<pre id="log"></pre>
|
|
603
|
-
<script>
|
|
604
|
-
const box = document.getElementById('log');
|
|
605
|
-
const h = cockpit.bash("tail -f ./build.log", {
|
|
606
|
-
background: true,
|
|
607
|
-
onOutput: c => { box.textContent += c; },
|
|
608
|
-
onExit: code => { box.textContent += '\\n[退出 ' + code + ']'; },
|
|
609
|
-
onError: msg => { box.textContent += '\\n[错误 ' + msg + ']'; },
|
|
610
|
-
});
|
|
611
|
-
// 需要时 h.kill()
|
|
612
|
-
</script>
|
|
613
|
-
\`\`\`
|
|
618
|
+
## 三、契约:window.cockpit
|
|
614
619
|
|
|
615
|
-
|
|
620
|
+
页面加载时 SDK 已就绪,无需引入任何库:
|
|
616
621
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
622
|
+
- \`cockpit.cwd: string\` —— 当前文件所在目录,相对路径命令默认在此执行。
|
|
623
|
+
- \`cockpit.bash(command, opts?)\` —— 执行一条 bash 命令(\`command\` 是整条 shell 串):
|
|
624
|
+
- **前台**(默认,短/离散命令)→ \`Promise<{ stdout, stderr, exitCode }>\`,\`await\` 后一次拿全部输出。
|
|
625
|
+
- **后台**(\`opts.background: true\`,长/实时命令)→ \`{ kill() }\`,经回调流式输出。
|
|
626
|
+
大/持续输出**必须**用后台——前台会把输出全缓存在内存里。
|
|
627
|
+
|
|
628
|
+
\`\`\`js
|
|
629
|
+
const h = cockpit.bash("npm run build", {
|
|
630
|
+
background: true,
|
|
631
|
+
onOutput: c => append(c),
|
|
632
|
+
onStderr: c => append(c),
|
|
633
|
+
onExit: code => append('\\n[退出 ' + code + ']'), // 命令跑完了,code 非零 = 失败
|
|
634
|
+
onError: msg => append('\\n[错误 ' + msg + ']'), // spawn / 基建失败
|
|
635
|
+
});
|
|
636
|
+
// 组件卸载或用户中止时 h.kill()
|
|
637
|
+
\`\`\`
|
|
620
638
|
|
|
621
|
-
|
|
622
|
-
- 用户没给 → 存到**当前聊天工作目录**(本次会话的 cwd)下的 \`<name>/\`
|
|
639
|
+
必守的几条:
|
|
623
640
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
641
|
+
1. **数据只走 \`cockpit.bash\`**(\`curl\` 取、\`curl -X POST\` 改、或读写文件),**绝不用
|
|
642
|
+
\`fetch(外部URL)\`** —— 同源沙箱会 CORS 拦截。(\`fetch('./app.jsx')\` 这种同目录相对请求没问题。)
|
|
643
|
+
2. **两类失败都要处理**:\`try/catch\` 抓到的**抛异常 = spawn/基建失败**(坏路径、连接断);没抛但
|
|
644
|
+
**\`exitCode !== 0\` = 命令跑了但失败**,要展示 \`stderr\` 或 stdout 里的错误体,别只写"失败"。
|
|
645
|
+
后台对应 \`onError\`(基建)+ \`onExit\` 非零 \`code\`(命令)。
|
|
646
|
+
3. **\`command\` 是 shell 串**:动态值/用户输入拼进去前必须校验或转义(shell 元字符会注入);结构化
|
|
647
|
+
入参一律走 base64(见骨架),别拼字符串;写库用**参数化**语句。
|
|
648
|
+
4. **资源引用**:同级相对(\`./app.jsx\`、\`./style.css\`、图片)和 CDN 绝对 URL 都能加载;根相对
|
|
649
|
+
(\`/assets/x.css\`)会 404 —— 服务端只托管 \`/html-lib/*\` 和 \`/apps/*\`。
|
|
630
650
|
|
|
631
|
-
|
|
651
|
+
## 四、本地数据:三类目录
|
|
632
652
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
\`curl\`, read/write files, and run scripts.
|
|
653
|
+
数据要落盘、要重开即用——**别每次打开都重新拉一遍**。所有持久化走 \`api.mjs\`,app 目录下按
|
|
654
|
+
「能不能重建」分三类,别平铺在一起:
|
|
636
655
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
contract below.
|
|
656
|
+
- \`cache/\` —— 能重建的(接口返回、原始 dump、派生索引)。删了只是慢。**整目录写进 \`.gitignore\`**。
|
|
657
|
+
- \`state/\` —— 重建不出来的(每日快照、用户手工维护的清单)。源接口给不了第二次,删了永久丢。**入库**。
|
|
658
|
+
- \`out/\` —— 给人看的产物(报告、导出),按日期命名,留档。
|
|
641
659
|
|
|
642
|
-
|
|
660
|
+
分类的意义全在 git 策略上:目录分了但 \`cache/\` 没 ignore、\`state/\` 没入库,等于没分。
|
|
643
661
|
|
|
644
|
-
|
|
662
|
+
骨架里已经做掉的(版本号、原子写、\`refresh\` 参数、取数时间戳、失败回落旧数据)不再重复,剩下这些
|
|
663
|
+
要你自己守:
|
|
645
664
|
|
|
646
|
-
1.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
(confirm / start / go / "write it" — any affirmative). Until then, only discuss — write
|
|
655
|
-
nothing to disk.
|
|
665
|
+
1. **会增长的目录必须有上限,并在脚本里执行**(留 N 条 / 超 X MB 就清)。只写在注释里的上限等于
|
|
666
|
+
没有,没人看管的 dump 目录涨到 GB 级是常态。
|
|
667
|
+
2. **落盘文件里不写绝对路径**(存相对 app 目录的路径),否则目录一挪索引全废;含邮箱等隐私的落盘
|
|
668
|
+
一律 ignore。
|
|
669
|
+
3. **留一个清空入口**(\`api.mjs\` 加个 \`clear_cache\` 模式 + 界面按钮),否则用户只能去文件管理器里
|
|
670
|
+
猜哪个文件能删。
|
|
671
|
+
4. 要用别的 app 的凭据 / 工具脚本时走 \`../<app>/\` 相对引用,并**先探测存在再用**;失败给一句照着做
|
|
672
|
+
就能修的提示(「去 xxx 重新登录」),别把原始报错甩给用户。
|
|
656
673
|
|
|
657
|
-
|
|
658
|
-
still stop once to present "understanding + directory + a 'shall I start writing?'" and
|
|
659
|
-
wait — but keep this round **lightweight**, don't re-interrogate.
|
|
674
|
+
## 五、主题
|
|
660
675
|
|
|
661
|
-
|
|
676
|
+
样式默认套 **Cockpit 主题**(\`index.html\` 里已经 link 了 \`/html-lib/theme.css\`)。**配色动手前先拉
|
|
677
|
+
一次这份文件**,它是变量的唯一真源,别凭记忆猜变量名:
|
|
662
678
|
|
|
663
|
-
|
|
679
|
+
\`\`\`bash
|
|
680
|
+
curl -fsS "{{BASE_URL}}/html-lib/theme.css"
|
|
681
|
+
\`\`\`
|
|
664
682
|
|
|
665
|
-
- \`
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
683
|
+
- \`:root\` 段是亮色取值、\`.dark\` 段是暗色。原始色阶(\`--slate-*\` / \`--teal-*\` / \`--red-*\` …)存的是
|
|
684
|
+
HSL 分量,所以半透明直接写 \`hsl(var(--green-9) / .12)\`。
|
|
685
|
+
- 前景色成对取(\`--card\`/\`--card-foreground\` …),**只有 \`--brand\` 是坑**:\`--brand-foreground\` 是
|
|
686
|
+
同色系色调,压在 \`--brand\` 填充上仅 1.5:1;**brand 填充上的文字改用 \`var(--background)\`**。
|
|
687
|
+
- \`cockpit-theme\` meta 让预览带一个浮动亮/暗切换按钮(默认右上角,用户可拖动吸附到任意一角,所以
|
|
688
|
+
不必为它在右上角留空位);**默认跟随 Cockpit 宿主主题**(Cockpit 外打开时按 meta 的
|
|
689
|
+
auto/light/dark),手动切换后**按 app 记住选择(跨刷新,优先于跟随)**,也可调
|
|
690
|
+
\`cockpit.toggleTheme()\`。
|
|
691
|
+
- **别自拍调色板、别上 Tailwind**;app 特有细节再补一小段 \`<style>\`。`,en:`---
|
|
692
|
+
name: html
|
|
693
|
+
description: "Generate an interactive local React app with the built-in cockpit bash SDK to fetch/update data"
|
|
694
|
+
argument-hint: "describe the app you want"
|
|
695
|
+
---
|
|
670
696
|
|
|
671
|
-
|
|
697
|
+
# Build a Cockpit app (a local React page)
|
|
672
698
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
failure (bad path, dropped connection); no throw but **\`exitCode !== 0\` = the command ran
|
|
677
|
-
and failed** — show \`stderr\` or the error body in stdout, don't just say "failed". Background:
|
|
678
|
-
\`onError\` (infra) + a non-zero \`code\` in \`onExit\` (command).
|
|
679
|
-
3. **\`command\` is a shell string**: validate or escape any dynamic/user input before
|
|
680
|
-
interpolating it (shell metacharacters inject); prefer a fixed command.
|
|
681
|
-
4. **Resource refs**: relative siblings (\`./app.jsx\`, \`./style.css\`, images) and absolute
|
|
682
|
-
CDN URLs load; root-relative (\`/assets/x.css\`) 404s.
|
|
699
|
+
A **local application**, not a one-off page: the preview injects a global \`window.cockpit\`
|
|
700
|
+
SDK — essentially the Bash tool exposed to the page, so buttons can \`curl\`, read/write files
|
|
701
|
+
and run scripts; data lands on disk inside the app directory and is there on reopen.
|
|
683
702
|
|
|
684
|
-
|
|
703
|
+
Zero build: React / Babel load from the same-origin \`/html-lib\` — no bundler, no deps, works offline.
|
|
685
704
|
|
|
686
|
-
|
|
687
|
-
<title>Page title</title>
|
|
688
|
-
<meta name="cockpit-name" content="short-name"> <!-- unique short name for /name; letters/digits/-_ only -->
|
|
689
|
-
<meta name="description" content="one line about the page">
|
|
690
|
-
<meta name="cockpit-icon" content="🔍"> <!-- emoji or icon url, optional -->
|
|
691
|
-
<meta name="cockpit-theme" content="auto"> <!-- enable the draggable light/dark toggle; auto = first load follows the OS, or light/dark -->
|
|
692
|
-
\`\`\`
|
|
705
|
+
## 1. Flow: align first, write only after confirmation (important)
|
|
693
706
|
|
|
694
|
-
|
|
707
|
+
**Don't write files up front.** Three steps:
|
|
695
708
|
|
|
696
|
-
|
|
709
|
+
1. **Align on the requirement** — restate your understanding of the app (what it does, where
|
|
710
|
+
data comes from, key features/interactions). Ask back only when the requirement is ambiguous
|
|
711
|
+
or has several reasonable readings; if it's already clear, just give a short summary — don't
|
|
712
|
+
interrogate.
|
|
713
|
+
2. **Tell the storage directory** — resolve \`<dir>/<name>/\` and tell the user exactly where it
|
|
714
|
+
goes and what it's called; they may reject or change it:
|
|
715
|
+
- **You always pick \`<name>\`** from the request (lowercase kebab-case, matching
|
|
716
|
+
\`cockpit-name\`) — the user only describes the app, they don't name it.
|
|
717
|
+
- The user gave a directory → \`<the given directory>/<name>/\`; none given → \`<name>/\` under
|
|
718
|
+
the **current chat working directory** (this session's cwd).
|
|
719
|
+
- Every file of the app lives in that one directory, with data split into \`cache/\`
|
|
720
|
+
\`state/\` \`out/\` per section 4. Don't scatter files elsewhere or invent a \`.cockpit-apps\`.
|
|
721
|
+
3. **Write only after confirmation** — \`Write\` the files once the user clearly agrees (confirm /
|
|
722
|
+
start / go / "write it" — any affirmative). Until then, only discuss.
|
|
723
|
+
|
|
724
|
+
Even if the user already spelled out requirement and directory in the \`/html\` call, still stop
|
|
725
|
+
once to present "understanding + directory + a 'shall I start writing?'" and wait — but keep that
|
|
726
|
+
round **lightweight**, don't re-interrogate.
|
|
727
|
+
|
|
728
|
+
## 2. Skeleton: three files, copy them
|
|
729
|
+
|
|
730
|
+
\`index.html\` — use this fixed shell, don't improvise:
|
|
697
731
|
|
|
698
732
|
\`\`\`html
|
|
699
|
-
|
|
700
|
-
<
|
|
701
|
-
<
|
|
702
|
-
<
|
|
703
|
-
<
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
733
|
+
<!doctype html>
|
|
734
|
+
<html>
|
|
735
|
+
<head>
|
|
736
|
+
<meta charset="utf-8">
|
|
737
|
+
<title>Page title</title>
|
|
738
|
+
<meta name="cockpit-name" content="short-name"> <!-- unique short name for /name; letters/digits/-_ -->
|
|
739
|
+
<meta name="description" content="one line about the page">
|
|
740
|
+
<meta name="cockpit-icon" content="🔍"> <!-- emoji or icon url, optional -->
|
|
741
|
+
<meta name="cockpit-theme" content="auto"> <!-- light/dark toggle, see section 5 -->
|
|
742
|
+
<link rel="stylesheet" href="/html-lib/theme.css">
|
|
743
|
+
</head>
|
|
744
|
+
<body>
|
|
745
|
+
<div id="root"></div>
|
|
746
|
+
<script src="/html-lib/react.production.min.js"></script>
|
|
747
|
+
<script src="/html-lib/react-dom.production.min.js"></script>
|
|
748
|
+
<script src="/html-lib/babel.min.js"></script>
|
|
749
|
+
<script>
|
|
750
|
+
(async () => {
|
|
751
|
+
const src = await (await fetch('./app.jsx')).text();
|
|
752
|
+
// MUST use classic runtime: the default automatic runtime injects an import of
|
|
753
|
+
// react/jsx-runtime, which — appended as a plain script — throws
|
|
754
|
+
// "Cannot use import statement outside a module".
|
|
755
|
+
const { code } = Babel.transform(src, { presets: [['react', { runtime: 'classic' }]] });
|
|
756
|
+
const s = document.createElement('script'); s.textContent = code; document.body.appendChild(s);
|
|
757
|
+
})();
|
|
758
|
+
</script>
|
|
759
|
+
</body>
|
|
760
|
+
</html>
|
|
713
761
|
\`\`\`
|
|
714
762
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
\`ReactDOM.createRoot(document.getElementById('root')).render(<App/>)\`.
|
|
718
|
-
- Style with the **Cockpit theme** by default: \`<link rel="stylesheet" href="/html-lib/theme.css">\`,
|
|
719
|
-
then use the semantic vars (\`var(--background)\` / \`--foreground\` / \`--card\` / \`--brand\` /
|
|
720
|
-
\`--muted-foreground\` / \`--border\` / \`--destructive\`; translucency via \`hsl(var(--green-9) / .12)\`).
|
|
721
|
-
The \`cockpit-theme\` meta above gives the preview a floating light/dark toggle (top-right by
|
|
722
|
-
default, draggable to any corner — so don't reserve top-right space for it); it **follows the
|
|
723
|
-
Cockpit host theme by default** (outside Cockpit, the meta's auto/light/dark decides), and a manual
|
|
724
|
-
toggle is **remembered per app across reloads and overrides the host**, or call \`cockpit.toggleTheme()\`
|
|
725
|
-
from your own UI. Don't invent a palette or add Tailwind; add a small \`<style>\` for app-specific bits.
|
|
763
|
+
All four meta lines matter: the HTML panel renders cards from them, and the console opens the
|
|
764
|
+
app via \`cockpit-name\` (\`/name\`).
|
|
726
765
|
|
|
727
|
-
|
|
766
|
+
\`app.jsx\` — global \`React\`, render at the end. **Opening reads the cache; only Refresh goes out**:
|
|
728
767
|
|
|
729
768
|
\`\`\`jsx
|
|
769
|
+
const { useState, useEffect, useCallback } = React;
|
|
770
|
+
|
|
730
771
|
function App() {
|
|
731
|
-
const [
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
772
|
+
const [data, setData] = useState(null);
|
|
773
|
+
const [busy, setBusy] = useState(false);
|
|
774
|
+
const [err, setErr] = useState(null);
|
|
775
|
+
|
|
776
|
+
const load = useCallback(async (refresh) => {
|
|
777
|
+
setBusy(true); setErr(null);
|
|
778
|
+
try {
|
|
779
|
+
const arg = btoa(JSON.stringify({ refresh: !!refresh })); // structured args as base64
|
|
780
|
+
const { stdout, stderr, exitCode } = await cockpit.bash(\`node ./api.mjs \${arg}\`);
|
|
781
|
+
if (exitCode !== 0) throw new Error(stderr || 'script failed'); // ran, but failed
|
|
782
|
+
const d = JSON.parse(stdout);
|
|
783
|
+
if (d.error && !d.stale) throw new Error(d.error);
|
|
784
|
+
setData(d);
|
|
785
|
+
if (d.stale) setErr(d.error); // old data is still shown
|
|
786
|
+
} catch (e) {
|
|
787
|
+
setErr(e.message); // a throw = spawn/infra failure. Keep existing data on screen
|
|
788
|
+
} finally { setBusy(false); }
|
|
789
|
+
}, []);
|
|
790
|
+
|
|
791
|
+
useEffect(() => { load(false); }, [load]); // on open: cache only, zero requests
|
|
792
|
+
|
|
793
|
+
if (!data) return <div>{err ?? 'Loading…'}</div>;
|
|
794
|
+
return (
|
|
795
|
+
<div>
|
|
796
|
+
<span>{data.cached ? 'cached' : 'updated'} \xb7 {new Date(data.fetchedAt).toLocaleString()}</span>
|
|
797
|
+
<button disabled={busy} onClick={() => load(true)}>{busy ? 'Refreshing…' : 'Refresh'}</button>
|
|
798
|
+
{err && <em>Refresh failed, showing older data: {err}</em>}
|
|
799
|
+
{/* render data */}
|
|
800
|
+
</div>
|
|
801
|
+
);
|
|
737
802
|
}
|
|
738
|
-
\`\`\`
|
|
739
803
|
|
|
740
|
-
|
|
804
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
805
|
+
\`\`\`
|
|
741
806
|
|
|
742
|
-
|
|
743
|
-
loops, a library, DB writes), write the backend as a **script file** next to the page and
|
|
744
|
-
call it — the HTML/JSX is the frontend, the script is the backend handler.
|
|
807
|
+
\`api.mjs\` — the backend handler: fetching, parsing and persistence live here; one JSON on stdout:
|
|
745
808
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
\`Buffer.from(arg,'base64')\` in the script) rather than string-building; use **parameterized**
|
|
751
|
-
statements for DB writes.
|
|
752
|
-
- The script returns JSON on stdout; the frontend does \`JSON.parse\`. \`Write\` the script first, then call it.
|
|
809
|
+
\`\`\`js
|
|
810
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
811
|
+
import { join, dirname } from 'node:path';
|
|
812
|
+
import { fileURLToPath } from 'node:url';
|
|
753
813
|
|
|
754
|
-
|
|
814
|
+
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
815
|
+
const CACHE = join(DIR, 'cache/data.json');
|
|
816
|
+
const VERSION = 1; // bump whenever the payload shape changes, or old cache feeds a new UI
|
|
755
817
|
|
|
756
|
-
|
|
818
|
+
const args = JSON.parse(Buffer.from(process.argv[2] || '', 'base64').toString() || '{}');
|
|
757
819
|
|
|
758
|
-
|
|
820
|
+
function readCache() {
|
|
821
|
+
if (!existsSync(CACHE)) return null;
|
|
822
|
+
const c = JSON.parse(readFileSync(CACHE, 'utf8'));
|
|
823
|
+
return c.v === VERSION ? c : null; // version mismatch = void it, act as if empty
|
|
824
|
+
}
|
|
825
|
+
function writeCache(payload) {
|
|
826
|
+
mkdirSync(dirname(CACHE), { recursive: true });
|
|
827
|
+
const tmp = \`\${CACHE}.\${process.pid}.tmp\`; // atomic: a kill mid-write leaves no half JSON
|
|
828
|
+
writeFileSync(tmp, JSON.stringify({ v: VERSION, fetchedAt: new Date().toISOString(), payload }));
|
|
829
|
+
renameSync(tmp, CACHE);
|
|
830
|
+
}
|
|
759
831
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
832
|
+
try {
|
|
833
|
+
const hit = args.refresh ? null : readCache();
|
|
834
|
+
if (hit) {
|
|
835
|
+
process.stdout.write(JSON.stringify({ ...hit.payload, cached: true, fetchedAt: hit.fetchedAt }));
|
|
836
|
+
} else {
|
|
837
|
+
const payload = await fetchFresh(args); // ← your fetching logic (curl / DB / commands)
|
|
838
|
+
writeCache(payload);
|
|
839
|
+
process.stdout.write(JSON.stringify({ ...payload, cached: false, fetchedAt: new Date().toISOString() }));
|
|
840
|
+
}
|
|
841
|
+
} catch (e) {
|
|
842
|
+
const hit = readCache(); // failed refresh: fall back to the old cache, labelled
|
|
843
|
+
process.stdout.write(JSON.stringify(hit
|
|
844
|
+
? { ...hit.payload, cached: true, stale: true, error: e.message, fetchedAt: hit.fetchedAt }
|
|
845
|
+
: { error: e.message }));
|
|
846
|
+
}
|
|
774
847
|
\`\`\`
|
|
775
848
|
|
|
776
|
-
|
|
849
|
+
- When one command is enough and the data isn't worth keeping (just viewing current state), skip
|
|
850
|
+
\`api.mjs\` and call \`cockpit.bash("curl -s …")\` directly. The moment data should survive a
|
|
851
|
+
reopen, use this skeleton rather than inventing your own.
|
|
852
|
+
- Same for other languages: \`python3 ./api.py\` / \`bash ./api.sh\` (explicit interpreter avoids
|
|
853
|
+
\`chmod +x\`); probe \`node\` / \`python3\` with \`command -v\` if unsure and fall back to shell.
|
|
854
|
+
- The cache key must include **every parameter that changes the result** (time range, fetch depth
|
|
855
|
+
…): use \`cache/<param>.json\` or bucket by key inside the file — otherwise a 7-day cache renders
|
|
856
|
+
in the 30-day view.
|
|
777
857
|
|
|
778
|
-
|
|
779
|
-
<pre id="log"></pre>
|
|
780
|
-
<script>
|
|
781
|
-
const box = document.getElementById('log');
|
|
782
|
-
const h = cockpit.bash("tail -f ./build.log", {
|
|
783
|
-
background: true,
|
|
784
|
-
onOutput: c => { box.textContent += c; },
|
|
785
|
-
onExit: code => { box.textContent += '\\n[exit ' + code + ']'; },
|
|
786
|
-
onError: msg => { box.textContent += '\\n[error ' + msg + ']'; },
|
|
787
|
-
});
|
|
788
|
-
// h.kill() when done
|
|
789
|
-
</script>
|
|
790
|
-
\`\`\`
|
|
858
|
+
## 3. Contract: window.cockpit
|
|
791
859
|
|
|
792
|
-
|
|
860
|
+
The SDK is ready on load — no library to import:
|
|
861
|
+
|
|
862
|
+
- \`cockpit.cwd: string\` — directory of the current file; relative-path commands run here.
|
|
863
|
+
- \`cockpit.bash(command, opts?)\` — run one bash command (\`command\` is a raw shell string):
|
|
864
|
+
- **Foreground** (default, short/discrete) → \`Promise<{ stdout, stderr, exitCode }>\`; \`await\` for the full output at once.
|
|
865
|
+
- **Background** (\`opts.background: true\`, long/live) → \`{ kill() }\`, streaming via callbacks.
|
|
866
|
+
Large/continuous output **must** use background — foreground buffers it all in memory.
|
|
867
|
+
|
|
868
|
+
\`\`\`js
|
|
869
|
+
const h = cockpit.bash("npm run build", {
|
|
870
|
+
background: true,
|
|
871
|
+
onOutput: c => append(c),
|
|
872
|
+
onStderr: c => append(c),
|
|
873
|
+
onExit: code => append('\\n[exit ' + code + ']'), // command finished; non-zero = failed
|
|
874
|
+
onError: msg => append('\\n[error ' + msg + ']'), // spawn / infra failure
|
|
875
|
+
});
|
|
876
|
+
// h.kill() on unmount or when the user aborts
|
|
877
|
+
\`\`\`
|
|
793
878
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
879
|
+
Rules every page must follow:
|
|
880
|
+
|
|
881
|
+
1. **Data only through \`cockpit.bash\`** (\`curl\` to read, \`curl -X POST\` to write, or read/write
|
|
882
|
+
files) — **never \`fetch(externalURL)\`**: the same-origin sandbox blocks it via CORS.
|
|
883
|
+
(\`fetch('./app.jsx')\` and other same-directory relative requests are fine.)
|
|
884
|
+
2. **Handle both failure modes**: a **throw** caught by \`try/catch\` = spawn/infra failure (bad
|
|
885
|
+
path, dropped connection); no throw but **\`exitCode !== 0\` = the command ran and failed** — show
|
|
886
|
+
\`stderr\` or the error body in stdout, don't just say "failed". Background: \`onError\` (infra) +
|
|
887
|
+
a non-zero \`code\` in \`onExit\` (command).
|
|
888
|
+
3. **\`command\` is a shell string**: validate or escape any dynamic/user input before interpolating
|
|
889
|
+
it (shell metacharacters inject); pass structured input as base64 (see the skeleton) instead of
|
|
890
|
+
string-building; use **parameterized** statements for DB writes.
|
|
891
|
+
4. **Resource refs**: relative siblings (\`./app.jsx\`, \`./style.css\`, images) and absolute CDN URLs
|
|
892
|
+
load; root-relative (\`/assets/x.css\`) 404s — the server only hosts \`/html-lib/*\` and \`/apps/*\`.
|
|
893
|
+
|
|
894
|
+
## 4. Local data: three directories
|
|
895
|
+
|
|
896
|
+
Data must land on disk and be there on reopen — **don't refetch everything on every open**. All
|
|
897
|
+
persistence goes through \`api.mjs\`; inside the app directory, split it by "can this be rebuilt?"
|
|
898
|
+
into three directories instead of one flat pile:
|
|
899
|
+
|
|
900
|
+
- \`cache/\` — rebuildable (API responses, raw dumps, derived indexes). Deleting it only costs time. **Gitignore the whole directory.**
|
|
901
|
+
- \`state/\` — NOT rebuildable (daily snapshots, hand-maintained lists). The source can't hand it to you twice; deleted means gone. **Commit it.**
|
|
902
|
+
- \`out/\` — artifacts for humans (reports, exports), date-named, kept.
|
|
903
|
+
|
|
904
|
+
The split only pays off through the git policy: directories separated but \`cache/\` not ignored and
|
|
905
|
+
\`state/\` not committed means nothing was separated.
|
|
906
|
+
|
|
907
|
+
What the skeleton already handles (versioning, atomic writes, the \`refresh\` flag, the fetch
|
|
908
|
+
timestamp, stale fallback) isn't repeated here. What's left is on you:
|
|
909
|
+
|
|
910
|
+
1. **Any growing directory needs a cap enforced in the script** (keep N entries / prune past X MB).
|
|
911
|
+
A cap that lives only in a comment is not a cap — unattended dump directories reach GB scale.
|
|
912
|
+
2. **Never write absolute paths into a file on disk** (store paths relative to the app directory),
|
|
913
|
+
or moving the directory voids the index; anything holding private data (emails …) is gitignored.
|
|
914
|
+
3. **Provide a clear/reset entry point** (a \`clear_cache\` mode in \`api.mjs\` + a button), or the
|
|
915
|
+
user's only option is guessing which file is safe to delete in a file manager.
|
|
916
|
+
4. To use another app's credentials / helper scripts, reference them as \`../<app>/\` and **probe
|
|
917
|
+
that they exist first**; on failure give one actionable line ("log in again at xxx") rather than
|
|
918
|
+
dumping the raw error.
|
|
919
|
+
|
|
920
|
+
## 5. Theme
|
|
921
|
+
|
|
922
|
+
Style with the **Cockpit theme** by default (\`index.html\` already links \`/html-lib/theme.css\`).
|
|
923
|
+
**Fetch that file once before you pick any colour** — it is the single source of truth for the
|
|
924
|
+
variables, so don't guess names from memory:
|
|
798
925
|
|
|
799
|
-
|
|
800
|
-
-
|
|
926
|
+
\`\`\`bash
|
|
927
|
+
curl -fsS "{{BASE_URL}}/html-lib/theme.css"
|
|
928
|
+
\`\`\`
|
|
801
929
|
|
|
802
|
-
|
|
803
|
-
|
|
930
|
+
- \`:root\` holds the light values, \`.dark\` the dark ones. The raw scales (\`--slate-*\` / \`--teal-*\` /
|
|
931
|
+
\`--red-*\` …) store HSL components, so translucency is just \`hsl(var(--green-9) / .12)\`.
|
|
932
|
+
- Take foregrounds in pairs (\`--card\`/\`--card-foreground\` …), **except \`--brand\`**:
|
|
933
|
+
\`--brand-foreground\` is a tint of the same hue and hits only 1.5:1 on a \`--brand\` fill — **use
|
|
934
|
+
\`var(--background)\` for text on a brand fill**.
|
|
935
|
+
- The \`cockpit-theme\` meta gives the preview a floating light/dark toggle (top-right by default,
|
|
936
|
+
draggable to any corner — so don't reserve top-right space for it); it **follows the Cockpit host
|
|
937
|
+
theme by default** (outside Cockpit, the meta's auto/light/dark decides), and a manual toggle is
|
|
938
|
+
**remembered per app across reloads and overrides the host**, or call \`cockpit.toggleTheme()\`.
|
|
939
|
+
- **Don't invent a palette or add Tailwind**; add a small \`<style>\` for app-specific bits.`},cg:{zh:`---
|
|
804
940
|
name: cg
|
|
805
941
|
description: "进入项目图谱(CodeGraph)探索模式:用预建的符号 + 调用图 + git 协同视图(search/callers/callees/impact/file/coedit + context/related/risk/affected)追代码、评估影响范围,返回坐标而非源码。"
|
|
806
942
|
---
|
|
@@ -1779,4 +1915,4 @@ When done, briefly report: skill slug, source path (\`<skills-dir>/<slug>/SKILL.
|
|
|
1779
1915
|
`,"utf-8"),f}catch{return null}}(a,j);return k?{name:a,path:k,content:null}:{name:a,path:null,content:j}}(a.cmd,m,r,n);s.set(a.i,function(a,b,c,d,e){if(b.path){let f=e?`[${k(a,d)}\xb7${b.name}]`:`[${b.name}]`;return c?`${f} ${c}`:f}let f=e?`[${k(a,d)}] `:"",g=c?"zh"===d?`,然后:${c}`:`, then: ${c}`:"";return`${f}${b.content??""}${g}`}(a.marker,i,f.join("\n"),m,q)),i.path&&!v.has(i.name)&&(v.add(i.name),u.push({name:i.name,path:i.path}))});let w=[];for(let a=0;a<o.length;a++)t.has(a)||w.push(s.get(a)??o[a]);let x=w.join("\n");if(u.length>0){let a="zh"===m?"请先读取以下 skill 文件,再据此执行:":"Read these skill files first, then act accordingly:",b="zh"===m?":":": ",c=u.map(a=>`- ${a.name}${b}${a.path}`).join("\n");x=`${x}
|
|
1780
1916
|
|
|
1781
1917
|
${a}
|
|
1782
|
-
${c}`}return x}function k(a,b){return"@"===a?"subagent":"zh"===b?"主会话":"main session"}},38561:(a,b,c)=>{c.d(b,{W:()=>d});let d=new Set(["Read","Grep","Glob","LS","WebSearch","WebFetch","NotebookRead","TodoRead","AskUserQuestion","ExitPlanMode","TodoWrite","Skill"])},82940:(a,b,c)=>{c.d(b,{AD:()=>j,GS:()=>l,HS:()=>m,Si:()=>n,Y6:()=>r,ah:()=>k,e5:()=>q,j$:()=>p,yQ:()=>o});let d=globalThis,e=d.__cockpitRunRegistry??(d.__cockpitRunRegistry=new Map),f=d.__cockpitRunListeners??(d.__cockpitRunListeners=new Map),g=d.__cockpitRunSeqByKey??(d.__cockpitRunSeqByKey=new Map);function h(a){for(let b of(a.seq+=1,a.keys))g.set(b,a.seq);return a.seq}function i(a,b){for(let c of a.keys){let a=f.get(c);if(a)for(let c of a)try{c(b)}catch{}}}function j(a,b,c,d){let f=e.get(a);if(f?.status==="running")return!1;let h=f?.seq??g.get(a)??0;if(f)for(let a of(f.evictTimer&&clearTimeout(f.evictTimer),f.keys))e.delete(a);let i=Date.now();return e.set(a,{keys:new Set([a]),cwd:b,status:"running",seq:h,events:[],startedAt:i,updatedAt:i}),c&&k(a,{type:"user",_human:!0,...d?{_turnId:d}:{},_ts:i,message:{role:"user",content:c}}),!0}function k(a,b){let c=e.get(a);c
|
|
1918
|
+
${c}`}return x}function k(a,b){return"@"===a?"subagent":"zh"===b?"主会话":"main session"}},38561:(a,b,c)=>{c.d(b,{W:()=>d});let d=new Set(["Read","Grep","Glob","LS","WebSearch","WebFetch","NotebookRead","TodoRead","AskUserQuestion","ExitPlanMode","TodoWrite","Skill"])},82940:(a,b,c)=>{c.d(b,{AD:()=>j,GS:()=>l,HS:()=>m,Si:()=>n,Y6:()=>r,ah:()=>k,e5:()=>q,j$:()=>p,yQ:()=>o});let d=globalThis,e=d.__cockpitRunRegistry??(d.__cockpitRunRegistry=new Map),f=d.__cockpitRunListeners??(d.__cockpitRunListeners=new Map),g=d.__cockpitRunSeqByKey??(d.__cockpitRunSeqByKey=new Map);function h(a){for(let b of(a.seq+=1,a.keys))g.set(b,a.seq);return a.seq}function i(a,b){for(let c of a.keys){let a=f.get(c);if(a)for(let c of a)try{c(b)}catch{}}}function j(a,b,c,d){let f=e.get(a);if(f?.status==="running")return!1;let h=f?.seq??g.get(a)??0;if(f)for(let a of(f.evictTimer&&clearTimeout(f.evictTimer),f.keys))e.delete(a);let i=Date.now();return e.set(a,{keys:new Set([a]),cwd:b,status:"running",seq:h,events:[],startedAt:i,updatedAt:i}),c&&k(a,{type:"user",_human:!0,...d?{_turnId:d}:{},_ts:i,message:{role:"user",content:c}}),!0}function k(a,b){let c=e.get(a);if(!c||"running"!==c.status)return;h(c);let d=function(a){if("object"!=typeof a||null===a||"usage_update"!==a.type)return null;let b=a.output_tokens;return"number"==typeof b&&Number.isFinite(b)?Math.max(0,Math.round(b)):null}(b);null!==d&&(c.outputTokens=Math.max(c.outputTokens??0,d)),("object"!=typeof b||null===b||"usage_update"!==b.type)&&c.events.push(b),c.updatedAt=Date.now(),i(c,{seq:c.seq,message:b})}function l(a,b){let c=e.get(a);c&&(c.sessionId=b,a!==b&&(c.keys.add(b),e.set(b,c)))}function m(a,b="idle"){let c=e.get(a);if(c){if("running"!==c.status){"idle"===c.status&&"error"===b&&(c.status="error");return}c.status=b,c.updatedAt=Date.now(),h(c),i(c,{seq:c.seq,message:{type:"run-ended",status:b}}),c.evictTimer&&clearTimeout(c.evictTimer),c.evictTimer=setTimeout(()=>{for(let a of c.keys)e.delete(a)},6e4)}}function n(a){let b=e.get(a);return b?{status:b.status,seq:b.seq,events:b.events.slice(),startedAt:b.startedAt,outputTokens:b.outputTokens}:null}function o(a){return e.get(a)?.status==="running"}function p(a){return e.get(a)?.sessionId??null}function q(a,b){let c=e.get(a);c&&(c.abort=b)}function r(a){let b=e.get(a);if(!b)return!1;try{b.abort?.()}catch{}return k(a,{type:"result",subtype:"stopped"}),m(a,"idle"),!0}}};
|