@yxzpro/agent-acta 2.14.4 → 2.14.6
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 +15 -5
- package/REFERENCE.md +10 -7
- package/core/service.mjs +9 -1
- package/package.json +1 -1
- package/plugin/plugin.mjs +125 -44
package/README.md
CHANGED
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
## 安装与快速上手
|
|
16
16
|
|
|
17
|
+
### 从 npm 装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i -g @yxzpro/agent-acta # 装完多出命令 agentacta 与 agentacta-mcp
|
|
21
|
+
agentacta --open # 起服务并打开 http://127.0.0.1:14570
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
⚠ 包名带作用域 `@yxzpro/`:裸名 `agent-acta` 与 npm 上已存在的 `agentacta`(别人的同类项目)被判定太像而发不出去。**命令名不受影响**,敲的还是 `agentacta`。国内镜像同步有先后(实测 npmmirror 还没同步到,腾讯云镜像已可回源),装不上就加 `--registry=https://registry.npmjs.org/`。
|
|
25
|
+
|
|
17
26
|
### 拿到的是压缩包(tgz)
|
|
18
27
|
|
|
19
28
|
```bash
|
|
@@ -96,16 +105,17 @@ dsh plugin --profile desktop add "git+https://atomgit.com/elegant001/agent-acta.
|
|
|
96
105
|
- **装完要彻底退出 DSH 再启动**(进程全没才算,只关窗口无效 —— 宿主有模块缓存)。
|
|
97
106
|
- 图标没出现,先按这两条查:① 宿主按 semver 判插件兼容(不匹配只进 `skippedBundles`、**不报错**,表现就是不出现);② `--profile` 有没有指对你实际在用的那个 profile。已在 **DSH 0.1.7-rc.2 / runtime 0.2.0-rc.1** 上验通。
|
|
98
107
|
|
|
99
|
-
|
|
108
|
+
**和命令行服务怎么共处**(这条最常问):插件**每次请求**现判 `127.0.0.1:14570` 在不在(环回探测约 1ms,结果缓存 2 秒),所以不用你选:
|
|
100
109
|
|
|
101
110
|
| 14570 | 面板给你什么 |
|
|
102
111
|
|---|---|
|
|
103
|
-
| 在跑 |
|
|
104
|
-
| 没在跑 | 插件自己在宿主进程里起一套(不监听端口、不写 pid
|
|
112
|
+
| 在跑 | **就是它的面板**,原样递进 iframe(数据、SSE、写操作全通)—— 本机仍然只有它那一个扫描器,插件不另起一套 |
|
|
113
|
+
| 没在跑 | 插件自己在宿主进程里起一套(不监听端口、不写 pid、不空闲自停),面板照常用 |
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
CLI 中途退出时面板会换成一小页「命令行服务已退出」,带一个**让插件接手**按钮 —— 点了才起,不自动起:自动起可能和你又把 CLI 拉回来撞成两个写者,这种决定留给人做。
|
|
116
|
+
**不需要**为了看面板去 `agentacta --stop` 或重启整个 DSH(早先那版要你这么做,体验很差,已改掉)。
|
|
107
117
|
|
|
108
|
-
|
|
118
|
+
**边界**(有意为之,不是漏做):插件不唤起悬浮卡片,`/`(宿主的认证兜底位)、`/api/shutdown`、`/api/client`、`/api/trae/capture-key`、`/widget` 这五条路由在宿主里根本不注册(防一次误触把宿主进程带走);面板保持自家深色皮、不跟宿主明暗切换;数据面照旧只读本机、零网络出口。细节在 REFERENCE 的「作为 DSH 插件接入」。
|
|
109
119
|
|
|
110
120
|
## 页面用法速查
|
|
111
121
|
|
package/REFERENCE.md
CHANGED
|
@@ -1096,7 +1096,7 @@ stdout 一行一条 JSON-RPC 消息,stderr 是诊断。回归见 `test/mcp-tes
|
|
|
1096
1096
|
### 装与生效
|
|
1097
1097
|
|
|
1098
1098
|
```
|
|
1099
|
-
dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm
|
|
1099
|
+
dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm 包名(已发布)
|
|
1100
1100
|
dsh plugin --profile desktop add "git+https://atomgit.com/elegant001/agent-acta.git#v2.14.4"
|
|
1101
1101
|
dsh plugin --profile desktop add "file:C:/绝对路径/yxzpro-agent-acta-2.14.4.tgz" # 或直接给包
|
|
1102
1102
|
dsh plugin --profile desktop list # 看装没装上
|
|
@@ -1109,17 +1109,20 @@ dsh plugin --profile desktop list #
|
|
|
1109
1109
|
- 图标不出现有两类原因,别再怀疑「路由没挂上」:① 宿主按 semver 判兼容性,不匹配**只进 `skippedBundles`、不报错**;② `dsh.client.inject` 必须列出声明目标 slot 的宿主包(`@deepseek-ai/dsh-client-ui-sidebar` / `-layout`,已在本包写好),少了它服务端路由、client 模块、两条 `slots.register` 全都「成功」而图标就是不出现。
|
|
1110
1110
|
|
|
1111
1111
|
本项目以 **MIT** 许可发布(`LICENSE`)。npm 上的名字是 **`@yxzpro/agent-acta`** —— 裸名 `agent-acta` 发不出去:npm 的相似性规则判它与已存在的 `agentacta`(Miraj Chokshi 的同类项目,2026-02 首发,MIT)太像,`PUT` 时直接 403 并建议改用作用域名。⚠ **这只改 npm 标识与 DSH 的 bundle 名**:CLI 命令仍是 `agentacta`,仓库名、面板标题、`/api/agent-acta/*` 那些 URL 都没动。发到 npm 之后装法可以直接写包名 —— `dsh plugin --profile desktop add "@yxzpro/agent-acta"`,这也是社区目录 `awesome-dsh-plugin.com/plugins.json` 里主流形态(4392 条中 2264 条走 npm 包名、1903 条走 `github:` 简写、`git+https` 全形式 0 条先例,详见 `DSH-PLUGIN-PLAN.md` §8.9)。代码里没有任何网络出口,也不上传日志内容:它只读你自己机器上那些 agent 已经落盘的文件。
|
|
1112
|
+
**已发布(2026-09-30)**:`@yxzpro/agent-acta@2.14.4` 在 npmjs 上公开可读(匿名 `GET` 包文档 200、空目录 `npm i @yxzpro/agent-acta` 实测装到 `plugin/` 三件齐、两个 bin 都在)。⚠ 镜像同步有先后:实测腾讯云镜像已能回源,**npmmirror 当时还是 404** —— 装不上就显式带 `--registry=https://registry.npmjs.org/`。
|
|
1112
1113
|
|
|
1113
|
-
###
|
|
1114
|
+
### 面板给什么内容:每次请求现判(09-30 由「装载时二选一」改成代理)
|
|
1114
1115
|
|
|
1115
|
-
|
|
1116
|
+
插件对 `127.0.0.1:14570` 的探测是**每次请求**做的(环回探测约 1ms,结果缓存 2 秒),不是装载时定死:
|
|
1116
1117
|
|
|
1117
|
-
|
|
|
1118
|
+
| 14570 | 行为 |
|
|
1118
1119
|
|---|---|
|
|
1119
|
-
|
|
|
1120
|
-
|
|
|
1120
|
+
| 在跑 | **代理**:入口/资产/API 全部转给那个服务,上游递出来的 HTML、JS、CSS 再过一遍 `rewriteRefs`(代理过来的面板若不改写,在 iframe 里照样白屏);`/api/events` 走**流式**透传(缓冲整条流等于把实时推送做成轮询);POST 的 body 要转出去,写操作才有效;转的时候 `host` 换成上游、剔掉 hop-by-hop 与 `content-length`。本机仍然只有它那一个扫描器 |
|
|
1121
|
+
| 没在跑 | 插件自己 `start({ mode:'hosted', listen:false })`:扫描 / 落索引 / SSE 心跳照跑,但不监听端口、不写 pid、不挂空闲自停 |
|
|
1121
1122
|
|
|
1122
|
-
|
|
1123
|
+
CLI 中途退出时不自动接手,入口换成一小页带按钮的「命令行服务已退出」,点 `POST /api/agent-acta/takeover` 才起 hosted ——
|
|
1124
|
+
自动起可能和「用户又把 CLI 起回来」撞成两个写者,这个决定留给人做。上游不在时代理回 **502**(不是 500、也不把插件弄崩)。
|
|
1125
|
+
注册数因此是 **109** 条(`ROUTES − DENY + EXTRA + 静态`,EXTRA = 入口 / 资产 / 接手三条)。
|
|
1123
1126
|
|
|
1124
1127
|
### 为什么面板不是「打开一个 URL」,以及静态资源为什么要中转
|
|
1125
1128
|
|
package/core/service.mjs
CHANGED
|
@@ -113,7 +113,15 @@ const sameModule = (a, b) => process.platform === 'win32' ? a.toLowerCase() ===
|
|
|
113
113
|
// 入口是**包根下**的 agent-acta-server.mjs。判据不能再拿 import.meta.url 去比:那比的是 core/service.mjs,
|
|
114
114
|
// 于是正常跑 CLI 时 argv[1] ≠ 它 ⇒ 判成 'lib' ⇒ 服务静默变成只读、一个字都不落盘。
|
|
115
115
|
const CLI_ENTRY = path.resolve(path.join(ROOT, 'agent-acta-server.mjs'));
|
|
116
|
-
|
|
116
|
+
// 判据**必须过 realpath**。npm 在 mac / linux 上把 bin 装成**符号链接**(`/opt/homebrew/bin/agentacta` → 包内那个 .mjs),
|
|
117
|
+
// 而 node 交给 `process.argv[1]` 的是「被敲进去的那个路径」、不解析软链 ⇒ 直接字符串相等会把 CLI 判成 'lib',
|
|
118
|
+
// 症状极难查:**任何参数都零输出、退出码 0**(`runCli()` 压根没被调用),Windows 却一切正常 ——
|
|
119
|
+
// 因为 npm 在 Windows 生成的是 `.cmd` / `.ps1` shim,传给 node 的是真路径。09-30 同事在 mac 上装完就是这样。
|
|
120
|
+
// `real` 做成可注入:这台机(Windows,未开开发者模式)建不了软链,单测靠假 realpath 才能钉住这条语义。
|
|
121
|
+
export function detectMode(argv1, entryPath, real = p => { try { return fs.realpathSync(p); } catch { return p; } }) {
|
|
122
|
+
return sameModule(real(path.resolve(argv1 || '')), real(entryPath)) ? 'cli' : 'lib';
|
|
123
|
+
}
|
|
124
|
+
let MODE = detectMode(process.argv[1], CLI_ENTRY);
|
|
117
125
|
// 诊断行的去向在这里定一次,之后全库一律走 log()(core/log.mjs):
|
|
118
126
|
// cli → stdout —— 终端体验与搬家前一致(--status / --doctor 的球与文字走原来的流)
|
|
119
127
|
// 其余 → stderr —— lib 的 stdout 是 importer 的**协议通道**(MCP over stdio),混进一行
|
package/package.json
CHANGED
package/plugin/plugin.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// AgentActa 的 DSH 插件壳(I11 批 2)。
|
|
2
2
|
//
|
|
3
|
-
// 形状是「URL 转接 +
|
|
3
|
+
// 形状是「URL 转接 + 逐条注册」,不是第二套服务:handler 要么转给 core 的 handle(req,res)(本机 hosted),
|
|
4
|
+
// 要么转给 14570 上那一个已在跑的服务(代理),两条都不自己再开扫描器。
|
|
4
5
|
// 静态文件也走 core 原有的 routePageFile / routeVendor(那套 path.resolve + 前缀校验防逃逸只有一份)。
|
|
5
6
|
// 装配层的生命周期(扫描 / 落索引 / SSE 心跳 / 归档轮)由 start({mode:'hosted'}) 开,
|
|
6
7
|
// 但**不监听端口、不写 pid、不挂空闲自停、不退出宿主进程** —— 见 core/service.mjs 的 MODE 三态。
|
|
7
8
|
import net from 'node:net';
|
|
9
|
+
import http from 'node:http';
|
|
8
10
|
import fs from 'node:fs';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
export const name = '@yxzpro/agent-acta';
|
|
19
21
|
export const inject = ['webServer'];
|
|
20
22
|
|
|
21
|
-
//
|
|
23
|
+
// 宿主里不该存在的五条:抢认证兜底的 `/`、退进程、起 Electron 壳、弹 UAC 抓密钥、悬浮球壳。
|
|
22
24
|
// 它们在 core 的路由表里照旧存在(CLI 要用),只是不给宿主注册。
|
|
23
25
|
// 导出是给守卫对的:test/cli-behavior-guard.mjs 断言「插件实际注册的集合 ≡ ROUTES − DENY + EXTRA」。
|
|
24
26
|
// 不导出的话,「插件悄悄少挂一条 API」这条红线只守住了 core 那半边。
|
|
@@ -28,24 +30,32 @@ export const inject = ['webServer'];
|
|
|
28
30
|
// exact 路由优先于兜底位,插件一旦把 '/' 挂成 exact,这一问就由面板 HTML 200 应答(无 cookie),
|
|
29
31
|
// 壳直接判「Desktop Host authentication failed」⇒ DSH 每次开机都起不来(2026-09-30 实测)。
|
|
30
32
|
export const HOSTED_DENY = ['/', '/api/shutdown', '/api/client', '/api/trae/capture-key', '/widget'];
|
|
31
|
-
//
|
|
33
|
+
// 面板的唯一入口路径:两支(本机 hosted 递 HTML / 代理上游递 HTML)都挂在这一条上,
|
|
32
34
|
// client.js 的 iframe src 就是它 —— 两支换路不换路径,才不会出现「图标点了但 404」。
|
|
33
35
|
export const ENTRY_PATH = '/api/agent-acta/entry';
|
|
34
36
|
// 静态资产的中转路径:`?p=page/x.js`。宿主前端在 `dsh-app://app` 下**只把 /api/* 代理到 webServer**,
|
|
35
37
|
// 所以面板里那些 `/page/…`、`/vendor/…` 引用在浏览器侧一律 404(curl 打宿主 http 口却是 200,别被骗)。
|
|
36
38
|
// 只能把它们收进 /api 命名空间,见下面的 rewriteRefs。
|
|
37
39
|
export const ASSET_PATH = '/api/agent-acta/asset';
|
|
38
|
-
|
|
40
|
+
// 「让插件接手」那条:代理模式下 CLI 服务中途停了,面板顶部会给出一个按钮,点它才起 hosted。
|
|
41
|
+
// 为什么要人点一下才起:自动起就可能和「用户又把 CLI 起回来」撞成两个写者,那个决定该由人负责。
|
|
42
|
+
export const TAKEOVER_PATH = '/api/agent-acta/takeover';
|
|
43
|
+
export const HOSTED_EXTRA = [ENTRY_PATH, ASSET_PATH, TAKEOVER_PATH];
|
|
39
44
|
|
|
40
45
|
// 双载体探测在测试里要能换掉(守卫不能依赖「用户有没有开着 14570」这种环境状态)。
|
|
41
46
|
let cliProbe = probeCliService;
|
|
42
47
|
export function setCliProbe(fn) { cliProbe = typeof fn === 'function' ? fn : probeCliService; }
|
|
48
|
+
// 上游端口也要能换:代理那一支的测试靠它指向一个假上游,不能去碰用户真在跑的 14570。
|
|
49
|
+
let cliPort = PORT;
|
|
50
|
+
export function setCliPort(p) { const n = Number(p); cliPort = Number.isFinite(n) && n > 0 ? n : PORT; }
|
|
43
51
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
52
|
+
// PLAN §8.8 的 A/B 之择,09-30 由 A 改判成 **B(代理转发)**:A 的体感是「装了个不能用」——
|
|
53
|
+
// 常驻服务才是常态,于是点开面板永远只看到一页说教,而给出的处置(停服务 + 重启整个 DSH)比看日志本身贵得多。
|
|
54
|
+
// B 之后:CLI 在跑就把它的面板原样递进来(本机仍然只有一个扫描器),没在跑才由插件自己起 hosted。
|
|
55
|
+
// 「在不在跑」改成**每次请求现判**(环回探测约 1ms,结果缓存 2 秒),所以说明页上那个假版本号也一并消了。
|
|
46
56
|
function probeCliService() {
|
|
47
57
|
return new Promise((resolve) => {
|
|
48
|
-
const s = net.connect({ host: '127.0.0.1', port:
|
|
58
|
+
const s = net.connect({ host: '127.0.0.1', port: cliPort, timeout: 600 });
|
|
49
59
|
s.on('connect', () => {
|
|
50
60
|
s.write('GET /api/version HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n');
|
|
51
61
|
let buf = '';
|
|
@@ -61,6 +71,16 @@ function probeCliService() {
|
|
|
61
71
|
});
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
let cliCache = { at: -1e9, up: null };
|
|
75
|
+
const CLI_TTL = 2000;
|
|
76
|
+
async function cliServiceUp(force) {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (!force && now - cliCache.at < CLI_TTL) return cliCache.up;
|
|
79
|
+
const up = await cliProbe();
|
|
80
|
+
cliCache = { at: now, up: up || null };
|
|
81
|
+
return cliCache.up;
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
// 静态资源白名单:启动时递归列目录,而不是抄一份常量表 ——
|
|
65
85
|
// 抄来的清单会随页面加组件/图标而过期(vendor/svg 下 54 个图标正是先前漏掉的那批)。
|
|
66
86
|
function staticFiles() {
|
|
@@ -147,14 +167,67 @@ export function assetTarget(req) {
|
|
|
147
167
|
return '/' + p;
|
|
148
168
|
}
|
|
149
169
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
// 上游递出来的 HTML / JS / CSS 里那些 `/page/`、`/vendor/` 一样到不了浏览器(iframe 在 dsh-app:// 下),
|
|
171
|
+
// 所以代理这一支也必须过同一遍改写 —— 复用 rewriteRefs,不另写第二套规则。
|
|
172
|
+
const HOP = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-length']);
|
|
173
|
+
|
|
174
|
+
export function proxy(req, res, upstreamPath) {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
const finish = () => resolve();
|
|
177
|
+
const up = http.request(
|
|
178
|
+
{
|
|
179
|
+
host: '127.0.0.1', port: cliPort, path: upstreamPath || req.url, method: req.method,
|
|
180
|
+
headers: { ...req.headers, host: '127.0.0.1:' + cliPort },
|
|
181
|
+
},
|
|
182
|
+
(ur) => {
|
|
183
|
+
const type = String(ur.headers['content-type'] || '');
|
|
184
|
+
const rewrite = ur.statusCode === 200 && REWRITEABLE.test(type);
|
|
185
|
+
const copyHeaders = () => {
|
|
186
|
+
res.statusCode = ur.statusCode;
|
|
187
|
+
for (const [k, v] of Object.entries(ur.headers)) if (!HOP.has(k.toLowerCase())) { try { res.setHeader(k, v); } catch {} }
|
|
188
|
+
};
|
|
189
|
+
if (!rewrite) {
|
|
190
|
+
// 原样透传:SSE(/api/events)靠这一支才能一段段 flush 出去,缓冲整条流等于把实时推送做成轮询
|
|
191
|
+
copyHeaders();
|
|
192
|
+
ur.on('data', (c) => { try { res.write(c); } catch {} });
|
|
193
|
+
ur.on('end', () => { try { res.end(); } catch {}; finish(); });
|
|
194
|
+
ur.on('error', () => { try { res.end(); } catch {}; finish(); });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const chunks = [];
|
|
198
|
+
ur.on('data', (c) => chunks.push(c));
|
|
199
|
+
ur.on('end', () => {
|
|
200
|
+
copyHeaders();
|
|
201
|
+
res.end(Buffer.from(rewriteRefs(Buffer.concat(chunks).toString('utf8')), 'utf8'));
|
|
202
|
+
finish();
|
|
203
|
+
});
|
|
204
|
+
ur.on('error', () => { try { res.statusCode = 502; res.end('agent-acta: 上游断流'); } catch {}; finish(); });
|
|
205
|
+
});
|
|
206
|
+
up.on('error', () => {
|
|
207
|
+
res.statusCode = 502;
|
|
208
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
209
|
+
res.end('agent-acta: 连不上 127.0.0.1:' + cliPort + '(命令行服务刚好退了?重新打开面板可让插件接手)');
|
|
210
|
+
finish();
|
|
211
|
+
});
|
|
212
|
+
if (req.method === 'GET' || req.method === 'HEAD') up.end(); else req.pipe(up); // POST 的 body 要转出去,写操作才有效
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
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
|
+
const plain = (res, code, msg) => { res.statusCode = code; res.setHeader('content-type', 'text/plain; charset=utf-8'); res.end('agent-acta: ' + msg); };
|
|
158
231
|
|
|
159
232
|
export function apply(ctx) {
|
|
160
233
|
let web = null;
|
|
@@ -176,45 +249,53 @@ export function apply(ctx) {
|
|
|
176
249
|
// 否则 apply() 只是「发起」了装配 —— 测试里就会看见注册表还是空的、断言空跑。
|
|
177
250
|
return ctx.inject(['webServer'], async (webCtx) => {
|
|
178
251
|
web = webCtx;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
// A 分支:不起 hosted、不挂业务路由,只留一条说明页。
|
|
183
|
-
reg('exact', ENTRY_PATH, (req, res) => {
|
|
184
|
-
res.statusCode = 200;
|
|
185
|
-
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
186
|
-
res.end(NOTICE(cli));
|
|
187
|
-
});
|
|
188
|
-
webCtx.logger?.info?.('[agent-acta] 检测到命令行服务在跑,插件只提供说明页');
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
252
|
+
// local = hosted 是否已在本进程起来。装载时探一次:CLI 在跑就先走代理,一个扫描器都不多起。
|
|
253
|
+
let local = false;
|
|
254
|
+
if (!(await cliServiceUp(true))) { await start({ mode: 'hosted', listen: false }); local = true; }
|
|
191
255
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
256
|
+
const api = async (req, res) => {
|
|
257
|
+
if (local) return handle(req, res);
|
|
258
|
+
if (await cliServiceUp()) return proxy(req, res);
|
|
259
|
+
return plain(res, 503, '命令行服务已退出、插件也还没接手 —— 重新打开面板,点「让插件接手」');
|
|
260
|
+
};
|
|
261
|
+
const entry = async (req, res) => {
|
|
262
|
+
if (local) return deliver(req, res, '/');
|
|
263
|
+
if (await cliServiceUp()) return proxy(req, res, '/');
|
|
264
|
+
res.statusCode = 200; res.setHeader('content-type', 'text/html; charset=utf-8'); res.end(TAKEOVER_PAGE());
|
|
265
|
+
};
|
|
266
|
+
const asset = async (req, res) => {
|
|
267
|
+
const target = assetTarget(req);
|
|
268
|
+
if (!target) return plain(res, 400, '资产请求的 p 参数不在白名单(要形如 page/x.js)');
|
|
269
|
+
if (local) return deliver(req, res, target);
|
|
270
|
+
if (await cliServiceUp()) return proxy(req, res, target);
|
|
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}');
|
|
281
|
+
};
|
|
195
282
|
|
|
196
283
|
// EXTRA 那几条有自己的 handler,不能让 passthrough 先占位:core 没这些路由,passthrough 会答 404;
|
|
197
284
|
// 而重复的 (kind,path) 在宿主里是 throw + **先注册的那条赢** ⇒ 真 handler 永远挂不上(302 那次就是这么坏的)。
|
|
198
|
-
const extraHandlers = {
|
|
199
|
-
[ENTRY_PATH]: (req, res) => deliver(req, res, '/'),
|
|
200
|
-
[ASSET_PATH]: (req, res) => {
|
|
201
|
-
const target = assetTarget(req);
|
|
202
|
-
if (!target) { res.statusCode = 400; res.setHeader('content-type', 'text/plain; charset=utf-8'); res.end('agent-acta: 资产请求的 p 参数不在白名单(要形如 page/x.js)'); return; }
|
|
203
|
-
deliver(req, res, target);
|
|
204
|
-
},
|
|
205
|
-
};
|
|
285
|
+
const extraHandlers = { [ENTRY_PATH]: entry, [ASSET_PATH]: asset, [TAKEOVER_PATH]: takeover };
|
|
206
286
|
|
|
207
287
|
for (const s of hostedRouteSpecs()) {
|
|
208
288
|
if (extraHandlers[s.path]) continue;
|
|
209
|
-
reg(s.kind, s.path,
|
|
289
|
+
reg(s.kind, s.path, api);
|
|
210
290
|
}
|
|
211
291
|
for (const p of HOSTED_EXTRA) reg('exact', p, extraHandlers[p]);
|
|
212
292
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
293
|
+
// 面板入口不 302 到宿主 http 口:iframe 从 dsh-app:// 跳 http://127.0.0.1 是跨源,实测被拦(空白)。
|
|
294
|
+
// 由 ENTRY_PATH 同源递 HTML(本机 hosted 那份或代理上游那份),页面里的静态引用统一翻译成 ASSET_PATH。
|
|
295
|
+
webCtx.logger?.info?.('[agent-acta] 就绪 v' + VERSION + ' (' + BUILD + '),路由 ' + registered.length + ' 条,'
|
|
296
|
+
+ (local ? '本机 hosted' : '代理 127.0.0.1:' + cliPort) + ',入口 ' + ENTRY_PATH);
|
|
217
297
|
|
|
218
|
-
|
|
298
|
+
// 只有真起了 hosted 才需要收尾停掉它(代理那一支没起任何东西)
|
|
299
|
+
web.effect(() => () => { if (local) stop('DSH 插件卸载'); });
|
|
219
300
|
});
|
|
220
301
|
}
|