@yxzpro/agent-acta 2.14.8 → 2.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/REFERENCE.md +4 -5
- package/package.json +1 -1
- package/page/topbar.js +7 -3
- package/plugin/client.js +2 -1
- package/plugin/plugin.mjs +39 -40
package/README.md
CHANGED
|
@@ -114,10 +114,10 @@ dsh plugin --profile desktop add "git+https://github.com/elegant01/agent-acta.gi
|
|
|
114
114
|
| 在跑 | **就是它的面板**,原样递进 iframe(数据、SSE、写操作全通)—— 本机仍然只有它那一个扫描器,插件不另起一套 |
|
|
115
115
|
| 没在跑 | 插件自己在宿主进程里起一套(不监听端口、不写 pid、不空闲自停),面板照常用 |
|
|
116
116
|
|
|
117
|
-
CLI
|
|
117
|
+
CLI 中途退出时插件**自己接手**:下一条请求现判探测到 14570 没了,就在宿主进程里起 hosted 并照常递面板,不再给你一页「让插件接手」按钮(2026-10-09 改掉;原先那页要点一下才起,理由是怕和你又把 CLI 拉回来撞成两个写者 —— 但装载那一刻本来就是「探不到就起」,拦在按钮上只是多一次点击)。接手之后本进程一直是那个扫描器,之后再把 CLI 起回来也不会换回代理(要换回得重启 DSH)。
|
|
118
118
|
**不需要**为了看面板去 `agentacta --stop` 或重启整个 DSH(早先那版要你这么做,体验很差,已改掉)。
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
**边界**(有意为之,不是漏做):插件不唤起悬浮卡片 —— 顶栏那颗「悬浮卡片」按钮在宿主里直接不渲染(入口 src 带 `?dsh=1`,页面据此收起),`/`(宿主的认证兜底位)、`/api/shutdown`、`/api/client`、`/api/trae/capture-key`、`/widget` 这五条路由在宿主里根本不注册(防一次误触把宿主进程带走);面板保持自家深色皮、不跟宿主明暗切换;数据面照旧只读本机、零网络出口。细节在 REFERENCE 的「作为 DSH 插件接入」。
|
|
121
121
|
|
|
122
122
|
## 页面用法速查
|
|
123
123
|
|
package/REFERENCE.md
CHANGED
|
@@ -1120,9 +1120,8 @@ dsh plugin --profile desktop list #
|
|
|
1120
1120
|
| 在跑 | **代理**:入口/资产/API 全部转给那个服务,上游递出来的 HTML、JS、CSS 再过一遍 `rewriteRefs`(代理过来的面板若不改写,在 iframe 里照样白屏);`/api/events` 走**流式**透传(缓冲整条流等于把实时推送做成轮询);POST 的 body 要转出去,写操作才有效;转的时候 `host` 换成上游、剔掉 hop-by-hop 与 `content-length`。本机仍然只有它那一个扫描器 |
|
|
1121
1121
|
| 没在跑 | 插件自己 `start({ mode:'hosted', listen:false })`:扫描 / 落索引 / SSE 心跳照跑,但不监听端口、不写 pid、不挂空闲自停 |
|
|
1122
1122
|
|
|
1123
|
-
CLI
|
|
1124
|
-
|
|
1125
|
-
注册数因此是 **109** 条(`ROUTES − DENY + EXTRA + 静态`,EXTRA = 入口 / 资产 / 接手三条)。
|
|
1123
|
+
CLI 中途退出时插件**自己接手**(2026-10-09 改判):`api`/`entry`/`asset` 三支每支都现判 —— 本机 hosted 已起就直接用,CLI 在跑就代理,两边都没有就 `startHosted()` 再接着用(用在途 promise 收口,SSE 重连 + 快照轮询 + 资产一起撞进来也只 start 一次)。原先这里是一页带按钮的「命令行服务已退出」,点 `POST /api/agent-acta/takeover` 才起 hosted,理由「自动起可能和用户又把 CLI 起回来撞成两个写者,这个决定留给人做」—— 但装载那一刻做的本来就是同一件事,拦在按钮上只是多一次点击,于是撤了。接手后 `local` 一直是 true,把 CLI 起回来也不会换回代理(要换回得重启 DSH)。上游不在时代理回 **502**(不是 500、也不把插件弄崩)。
|
|
1124
|
+
注册数因此是 **108** 条(`ROUTES − DENY + EXTRA + 静态`,EXTRA = 入口 / 资产两条)。时序验收在 `test/plugin-takeover-test.mjs`(假上游 → 关掉 → 下一条请求要递出真面板,不是说明页)。
|
|
1126
1125
|
|
|
1127
1126
|
### 为什么面板不是「打开一个 URL」,以及静态资源为什么要中转
|
|
1128
1127
|
|
|
@@ -1136,11 +1135,11 @@ CLI 中途退出时不自动接手,入口换成一小页带按钮的「命令
|
|
|
1136
1135
|
|
|
1137
1136
|
### 宿主里刻意不存在的四条路由
|
|
1138
1137
|
|
|
1139
|
-
`/api/shutdown`(原义=退出进程)、`/api/client`(spawn Electron 悬浮卡片壳)、`/api/trae/capture-key`(会弹 UAC)、`/widget`(卡片页)。它们在 core 的路由表里照旧留着给 CLI 用,只是不往宿主注册 —— 否则一次误触会用掉用户的 DSH
|
|
1138
|
+
`/api/shutdown`(原义=退出进程)、`/api/client`(spawn Electron 悬浮卡片壳)、`/api/trae/capture-key`(会弹 UAC)、`/widget`(卡片页)。它们在 core 的路由表里照旧留着给 CLI 用,只是不往宿主注册 —— 否则一次误触会用掉用户的 DSH。顶栏那颗「悬浮卡片」按钮跟着一起藏:入口 src 是 `/api/agent-acta/entry?dsh=1`(iframe 的文档 URL 就是它,两支都不用把查询串转给上游),`page/topbar.js` 的 setup 读 `location.search` 得 `isDsh`,按钮挂 `v-if="!isDsh"` —— 否则点了只剩一句「唤出失败」。Origin 闸在 hosted 下不再拿「端口等于 14570」当同源标识(宿主端口每次动态),改收「http(s) + 环回主机名(端口不限)」与 `dsh-app:`,且**只管写操作**,与 CLI 同口径。
|
|
1140
1139
|
|
|
1141
1140
|
### 边界与回归
|
|
1142
1141
|
|
|
1143
|
-
不跟宿主明暗切换(面板是自家深色皮,且实测注入 `--dsw-alias-*`
|
|
1142
|
+
不跟宿主明暗切换(面板是自家深色皮,且实测注入 `--dsw-alias-*` 也零变化);不唤起悬浮卡片(连顶栏那颗按钮都不渲染);无网络出口。验收:`test/plugin-delivery-test.mjs`(静态:改写覆盖 / 产物可达 / 白名单挡穿越)+ `test/plugin-hosted-test.mjs`(隔离端口 14631 + `mkdtemp` 数据目录里驱动 `apply()`:108 条注册、`exact /` 不在表里、入口 200 非 302、资产与图标真取到、Origin 闸、不写 pid、卸载撤干净)+ `test/plugin-takeover-test.mjs`(隔离端口 14637:假上游在跑时走代理、关掉之后下一条请求要静默接手并递出真面板),三者都在 selftest 的 `cli` 组;老 CLI 那一面由 `test/cli-behavior-guard.mjs` 七类不变式守着。宿主侧逐条取证与推翻记录在 `DSH-PLUGIN-PLAN.md` §8。
|
|
1144
1143
|
|
|
1145
1144
|
## 解析器自检(侧栏「解析器自检」/ `GET /api/selftest`,R32)
|
|
1146
1145
|
|
package/package.json
CHANGED
package/page/topbar.js
CHANGED
|
@@ -16,8 +16,9 @@ AACTA.Topbar = {
|
|
|
16
16
|
<div class="brand"><span class="logo">A</span>AgentActa</div>
|
|
17
17
|
<div class="top-right">
|
|
18
18
|
<!-- 唤出桌面悬浮卡片:卡片被 ✕ 关掉后壳可能还活着(被面板窗吊着),命令行之外给页面一个入口。
|
|
19
|
-
服务端 POST /api/client → runClient()(幂等):壳在就聚焦/重建卡片,不在就拉起新壳。
|
|
20
|
-
|
|
19
|
+
服务端 POST /api/client → runClient()(幂等):壳在就聚焦/重建卡片,不在就拉起新壳。
|
|
20
|
+
DSH 里不挂这条路由(HOSTED_DENY),入口 src 带 ?dsh=1,这颗按钮点了只会报错 —— 直接不渲染。 -->
|
|
21
|
+
<el-button v-if="!isDsh" size="small" plain :loading="clientLaunching" title="打开桌面悬浮卡片(右上角常驻的小窗/水球)" @click="$emit('launch-client')">悬浮卡片</el-button>
|
|
21
22
|
<el-dropdown trigger="click" placement="bottom-end" @command="v => $emit('set-idle', v)">
|
|
22
23
|
<span class="live-tag idle" :class="{idleOn: idleExitMs > 0}" :title="'页面全关且没有新日志持续这么久后,服务自己退出(下次开会话会被 hook 自动拉起)。当前:' + idleText">
|
|
23
24
|
<span class="dot"></span>{{ idleText }}<el-icon class="caret"><arrow-down></arrow-down></el-icon>
|
|
@@ -56,6 +57,9 @@ AACTA.Topbar = {
|
|
|
56
57
|
},
|
|
57
58
|
emits: ['launch-client', 'set-idle', 'set-scan'],
|
|
58
59
|
setup() {
|
|
59
|
-
|
|
60
|
+
// 载体标记(不是状态,所以不进根的 setup):DSH 插件的 iframe src 是 /api/agent-acta/entry?dsh=1,
|
|
61
|
+
// 宿主里 /api/client 没注册,那颗按钮只剩报错;浏览器标签页与悬浮卡片「面板 ↗」都不带这个参数,照旧渲染。
|
|
62
|
+
const isDsh = new URLSearchParams(location.search).has('dsh');
|
|
63
|
+
return { isDsh };
|
|
60
64
|
}
|
|
61
65
|
};
|
package/plugin/client.js
CHANGED
|
@@ -11,7 +11,8 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
// 面板入口:由服务端**同源递出**面板 HTML(宿主前端在 dsh-app://app,只代理 /api/*,
|
|
12
12
|
// 而 iframe 302 到 http 口是跨源、实测被拦成空白 —— 见 DSH-PLUGIN-PLAN.md §8.6 第七轮)。
|
|
13
13
|
// 页面里的 /page/、/vendor/ 引用由服务端递送时翻译成 /api/agent-acta/asset?p=…,浏览器侧一行都不用改。
|
|
14
|
-
|
|
14
|
+
// ?dsh=1 是给页面读的载体标记:iframe 的文档 URL 就是它,顶栏据此不渲染「悬浮卡片」(宿主里 /api/client 没注册)。
|
|
15
|
+
const PAGE_URL = '/api/agent-acta/entry?dsh=1';
|
|
15
16
|
|
|
16
17
|
// 没有 icon 这个注册参数 —— 注册的组件本身就是侧栏那颗图标,宿主按 { size, active } 喂它。
|
|
17
18
|
function PanelIcon({ size, active }) {
|
package/plugin/plugin.mjs
CHANGED
|
@@ -21,7 +21,9 @@ export const name = '@yxzpro/agent-acta';
|
|
|
21
21
|
export const inject = ['webServer'];
|
|
22
22
|
|
|
23
23
|
// 宿主里不该存在的五条:抢认证兜底的 `/`、退进程、起 Electron 壳、弹 UAC 抓密钥、悬浮球壳。
|
|
24
|
-
// 它们在 core 的路由表里照旧存在(CLI
|
|
24
|
+
// 它们在 core 的路由表里照旧存在(CLI 要用),只是不给宿主注册。顶栏「悬浮卡片」那颗按钮打的是
|
|
25
|
+
// /api/client:hosted 那支压根没这条,代理那支虽能打通,但把桌面悬浮壳开在宿主外头也不是插件该做的事
|
|
26
|
+
// ⇒ 页面按入口 src 上的 ?dsh=1 把它藏掉(两支一致,见 client.js 与 page/topbar.js)。
|
|
25
27
|
// 导出是给守卫对的:test/cli-behavior-guard.mjs 断言「插件实际注册的集合 ≡ ROUTES − DENY + EXTRA」。
|
|
26
28
|
// 不导出的话,「插件悄悄少挂一条 API」这条红线只守住了 core 那半边。
|
|
27
29
|
//
|
|
@@ -32,15 +34,14 @@ export const inject = ['webServer'];
|
|
|
32
34
|
export const HOSTED_DENY = ['/', '/api/shutdown', '/api/client', '/api/trae/capture-key', '/widget'];
|
|
33
35
|
// 面板的唯一入口路径:两支(本机 hosted 递 HTML / 代理上游递 HTML)都挂在这一条上,
|
|
34
36
|
// client.js 的 iframe src 就是它 —— 两支换路不换路径,才不会出现「图标点了但 404」。
|
|
37
|
+
// src 上带的 ?dsh=1 是给页面读的载体标记(页面据此藏掉「悬浮卡片」那颗按钮,见 client.js 与 page/topbar.js):
|
|
38
|
+
// iframe 的文档 URL 就是这个 src,所以递 HTML 那两支都不必把查询串转给上游。
|
|
35
39
|
export const ENTRY_PATH = '/api/agent-acta/entry';
|
|
36
40
|
// 静态资产的中转路径:`?p=page/x.js`。宿主前端在 `dsh-app://app` 下**只把 /api/* 代理到 webServer**,
|
|
37
41
|
// 所以面板里那些 `/page/…`、`/vendor/…` 引用在浏览器侧一律 404(curl 打宿主 http 口却是 200,别被骗)。
|
|
38
42
|
// 只能把它们收进 /api 命名空间,见下面的 rewriteRefs。
|
|
39
43
|
export const ASSET_PATH = '/api/agent-acta/asset';
|
|
40
|
-
|
|
41
|
-
// 为什么要人点一下才起:自动起就可能和「用户又把 CLI 起回来」撞成两个写者,那个决定该由人负责。
|
|
42
|
-
export const TAKEOVER_PATH = '/api/agent-acta/takeover';
|
|
43
|
-
export const HOSTED_EXTRA = [ENTRY_PATH, ASSET_PATH, TAKEOVER_PATH];
|
|
44
|
+
export const HOSTED_EXTRA = [ENTRY_PATH, ASSET_PATH];
|
|
44
45
|
|
|
45
46
|
// 双载体探测在测试里要能换掉(守卫不能依赖「用户有没有开着 14570」这种环境状态)。
|
|
46
47
|
let cliProbe = probeCliService;
|
|
@@ -206,27 +207,13 @@ export function proxy(req, res, upstreamPath) {
|
|
|
206
207
|
up.on('error', () => {
|
|
207
208
|
res.statusCode = 502;
|
|
208
209
|
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
209
|
-
res.end('agent-acta: 连不上 127.0.0.1:' + cliPort + '
|
|
210
|
+
res.end('agent-acta: 连不上 127.0.0.1:' + cliPort + '(命令行服务刚好退了?下一条请求插件会自己接手)');
|
|
210
211
|
finish();
|
|
211
212
|
});
|
|
212
213
|
if (req.method === 'GET' || req.method === 'HEAD') up.end(); else req.pipe(up); // POST 的 body 要转出去,写操作才有效
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
const PAGE_STYLE = 'body{font:14px/1.7 ui-monospace,Consolas,monospace;background:#12161d;color:#dfe6ef;padding:22px;max-width:46rem}code{background:#ffffff14;padding:2px 6px;border-radius:4px}button{font:inherit;background:#2d6cdf;color:#fff;border:0;border-radius:6px;padding:8px 16px;cursor:pointer}';
|
|
217
|
-
|
|
218
|
-
// 代理模式下 CLI 中途退了才见得到这一页:给一个明确的按钮,点了才起 hosted(不自动起 —— 自动起可能和
|
|
219
|
-
// 「用户又把 CLI 起回来」撞成两个写者,那个决定该由人负责)。
|
|
220
|
-
const TAKEOVER_PAGE = () => `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>AgentActa</title>
|
|
221
|
-
<style>${PAGE_STYLE}</style>
|
|
222
|
-
<body><h3>命令行那版服务已退出</h3>
|
|
223
|
-
<p>插件刚才是把 <code>127.0.0.1:${cliPort}</code> 的面板原样递进来的;那个服务不在了,所以这里没有数据可读。</p>
|
|
224
|
-
<p>要插件自己接手(在本进程里起一套扫描与索引,仍不占端口、不写 pid):
|
|
225
|
-
<button id="go">让插件接手</button>
|
|
226
|
-
<script>document.getElementById('go').onclick=function(){var b=this;b.disabled=1;b.textContent='启动中…';
|
|
227
|
-
fetch('${TAKEOVER_PATH}',{method:'POST'}).then(function(r){return r.json()}).then(function(){location.reload()},function(){location.reload()})};</script>
|
|
228
|
-
</body></html>`;
|
|
229
|
-
|
|
230
217
|
const plain = (res, code, msg) => { res.statusCode = code; res.setHeader('content-type', 'text/plain; charset=utf-8'); res.end('agent-acta: ' + msg); };
|
|
231
218
|
|
|
232
219
|
export function apply(ctx) {
|
|
@@ -251,38 +238,50 @@ export function apply(ctx) {
|
|
|
251
238
|
web = webCtx;
|
|
252
239
|
// local = hosted 是否已在本进程起来。装载时探一次:CLI 在跑就先走代理,一个扫描器都不多起。
|
|
253
240
|
let local = false;
|
|
254
|
-
|
|
241
|
+
// 上游中途退了就直接接手 —— 装载那一刻做的本来是同一件事(探不到 CLI 就起 hosted),这里只是把
|
|
242
|
+
// 它推迟到第一次真的需要数据的请求,不再拦一道「先点按钮」。start() 不 listen、不写 pid,
|
|
243
|
+
// 代价是多一个本进程扫描器;和「用户又把 CLI 起回来」撞成两个写者这条,与装载时那支同风险。
|
|
244
|
+
// 并发的请求(SSE 重连 + 快照轮询 + 资产)会一起撞进来,所以用在途 promise 收口成一次 start()。
|
|
245
|
+
let taking = null;
|
|
246
|
+
const startHosted = () => {
|
|
247
|
+
if (!taking) {
|
|
248
|
+
taking = start({ mode: 'hosted', listen: false })
|
|
249
|
+
.then(() => {
|
|
250
|
+
local = true;
|
|
251
|
+
webCtx.logger?.info?.('[agent-acta] hosted 就绪(本进程扫描)v' + VERSION + ' (' + BUILD + ')');
|
|
252
|
+
})
|
|
253
|
+
.catch((e) => { taking = null; throw e; });
|
|
254
|
+
}
|
|
255
|
+
return taking;
|
|
256
|
+
};
|
|
257
|
+
if (!(await cliServiceUp(true))) await startHosted();
|
|
258
|
+
|
|
259
|
+
// 每支请求都现判:本机 hosted 已起 → 直接用;CLI 在跑 → 代理过去;两边都没有 → 接手后再用。
|
|
260
|
+
const sourceOf = async () => {
|
|
261
|
+
if (local) return 'local';
|
|
262
|
+
if (await cliServiceUp()) return 'proxy';
|
|
263
|
+
await startHosted();
|
|
264
|
+
return 'local';
|
|
265
|
+
};
|
|
255
266
|
|
|
256
267
|
const api = async (req, res) => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return plain(res, 503, '命令行服务已退出、插件也还没接手 —— 重新打开面板,点「让插件接手」');
|
|
268
|
+
let src; try { src = await sourceOf(); } catch { return plain(res, 503, '插件起 hosted 失败,看宿主日志'); }
|
|
269
|
+
return src === 'local' ? handle(req, res) : proxy(req, res);
|
|
260
270
|
};
|
|
261
271
|
const entry = async (req, res) => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
res.statusCode = 200; res.setHeader('content-type', 'text/html; charset=utf-8'); res.end(TAKEOVER_PAGE());
|
|
272
|
+
let src; try { src = await sourceOf(); } catch { return plain(res, 503, '插件起 hosted 失败,看宿主日志'); }
|
|
273
|
+
return src === 'local' ? deliver(req, res, '/') : proxy(req, res, '/');
|
|
265
274
|
};
|
|
266
275
|
const asset = async (req, res) => {
|
|
267
276
|
const target = assetTarget(req);
|
|
268
277
|
if (!target) return plain(res, 400, '资产请求的 p 参数不在白名单(要形如 page/x.js)');
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return plain(res, 503, '命令行服务已退出,插件也还没接手');
|
|
272
|
-
};
|
|
273
|
-
const takeover = async (req, res) => {
|
|
274
|
-
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
275
|
-
if (local) { res.end('{"ok":true,"already":true}'); return; }
|
|
276
|
-
if (await cliServiceUp(true)) { res.statusCode = 409; res.end('{"ok":false,"error":"命令行服务又在跑了,插件继续代理即可"}'); return; }
|
|
277
|
-
await start({ mode: 'hosted', listen: false });
|
|
278
|
-
local = true;
|
|
279
|
-
webCtx.logger?.info?.('[agent-acta] 接手:hosted 就绪 v' + VERSION + ' (' + BUILD + ')');
|
|
280
|
-
res.end('{"ok":true,"started":true}');
|
|
278
|
+
let src; try { src = await sourceOf(); } catch { return plain(res, 503, '插件起 hosted 失败,看宿主日志'); }
|
|
279
|
+
return src === 'local' ? deliver(req, res, target) : proxy(req, res, target);
|
|
281
280
|
};
|
|
282
281
|
|
|
283
282
|
// EXTRA 那几条有自己的 handler,不能让 passthrough 先占位:core 没这些路由,passthrough 会答 404;
|
|
284
283
|
// 而重复的 (kind,path) 在宿主里是 throw + **先注册的那条赢** ⇒ 真 handler 永远挂不上(302 那次就是这么坏的)。
|
|
285
|
-
const extraHandlers = { [ENTRY_PATH]: entry, [ASSET_PATH]: asset
|
|
284
|
+
const extraHandlers = { [ENTRY_PATH]: entry, [ASSET_PATH]: asset };
|
|
286
285
|
|
|
287
286
|
for (const s of hostedRouteSpecs()) {
|
|
288
287
|
if (extraHandlers[s.path]) continue;
|