@yxzpro/agent-acta 2.14.4 → 2.14.8

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/LOGFORMATS.md CHANGED
@@ -714,6 +714,12 @@ codebuddy / workbuddy 优先按上表的 `projects/` 解析(能拿到真实 pr
714
714
 
715
715
  ⚠️ 「装了 CodeBuddy CN 却一条看不到」**不是**该去 `%APPDATA%\CodeBuddy CN` 找 —— 那目录里真的没有正文;
716
716
  是 `%LOCALAPPDATA%\CodeBuddyExtension` 不存在(没装扩展)或那棵 tree 里没有 `history/` 结构。
717
+ 顶层那份 `codebuddy-sessions.vscdb` 名字最骗人(本机拆开验过):整库一张 `ItemTable`,一条会话只有一行
718
+ `session:<id>` = `{conversationId, cwd, userId, title, status, createdAt, updatedAt}` —— 只是索引,没有消息也没有 token。
719
+
720
+ 扩展根没挂上时现在有话说:诊断行、`--doctor` 与 `[discover]` 日志都会点名盘上**实际看到的目录名**,
721
+ 把四种成因分开讲(没装扩展 / 没有 `Data/` / `<acct>` 底下的 `<host>` 名不在 `vscode`·`codebuddyide`·`jetbrains`
722
+ 白名单里 / `<host>` 底下没有 `history/`),不用再远程让人敲命令问盘 —— 判据与措辞都在 `buddyExtMissingWhy`。
717
723
 
718
724
  为什么合成一行而不是并排两行:两边是同一个产品在不同宿主里的形态,拆两行会把**同一个项目的历史劈成两半**
719
725
  (按 agent 筛就漏一边,用户只会觉得「应用里的会话怎么少了一半」)。代价是一行里两类卡片的完整度不同
package/README.en.md ADDED
@@ -0,0 +1,248 @@
1
+ # AgentActa
2
+
3
+ English | [中文](./README.md)
4
+
5
+ A local request-log dashboard for AI agents: it aggregates the session logs that various AI agents write to disk, and shows per-turn/per-request latency, tokens, cache hits, context usage and tool-call detail in a browser.
6
+
7
+ - A local long-running service + SSE live push — **no external dependencies, no network egress, purely read-only** (it injects nothing and never touches any agent's config)
8
+ - One page for 24 agents: atomcode / codebuddy / workbuddy / claude / codex / cursor / trae / qoder / opencode / gemini / copilot / windsurf / codearts / kimi / dsh / zcode / doubao / hermes / devin / minimax / mimocode / kilo / openclaw / cline
9
+ - Plus a floating desktop widget (a small Electron window): today/all-time tokens + the latest card stream
10
+
11
+ Requirements: `node` on the machine (`node -v` works, `>=22.5`). **Windows is the primary verified platform**; the code paths for mac / Linux / HarmonyOS PC (per-platform candidate directories, self-healing after cross-platform moves, POSIX permissions) are in place and pinned by regression tests, but have not been run end-to-end on real machines — before switching platforms, read the "Cross-platform" section of [REFERENCE.md](REFERENCE.md), which lists where the platforms differ and the two or three things you must handle by hand.
12
+
13
+ For the full semantics, internals and troubleshooting guide see **[REFERENCE.md](REFERENCE.md)**; for the field semantics and parsing boundaries of each agent's log format see **[LOGFORMATS.md](LOGFORMATS.md)**. (Both are currently Chinese-only.)
14
+
15
+ ## Install & quick start
16
+
17
+ ### From npm
18
+
19
+ ```bash
20
+ npm i -g @yxzpro/agent-acta # installs the `agentacta` and `agentacta-mcp` commands
21
+ agentacta --open # start the service and open http://127.0.0.1:14570
22
+ ```
23
+
24
+ ⚠ The package name carries the `@yxzpro/` scope: the bare name `agent-acta` was rejected as too similar to the existing `agentacta` package (someone else's similar project). **The command name is unaffected** — you still type `agentacta`. Registry mirrors sync at different speeds; if install fails, add `--registry=https://registry.npmjs.org/`.
25
+
26
+ ### From a tarball (tgz)
27
+
28
+ ```bash
29
+ npm i -g agent-acta-<version>.tgz # installs the `agentacta` command
30
+ agentacta --install-hooks # optional: auto-start the service when a session begins
31
+ agentacta --open # start the service and open http://127.0.0.1:14570
32
+ ```
33
+
34
+ ### From a source checkout
35
+
36
+ ```bash
37
+ npm i -g <this directory> # symlink install; edits take effect immediately
38
+ node <this directory>/agent-acta-server.mjs --ensure # or skip npm entirely and start the service with one command
39
+ ```
40
+
41
+ Then open <http://127.0.0.1:14570> in a browser. Supported agents are discovered automatically — no configuration needed.
42
+
43
+ ## Command reference
44
+
45
+ | Command | What it does |
46
+ |---|---|
47
+ | `agentacta` | Run the service in the foreground (the service itself) |
48
+ | `agentacta --ensure` | Make sure the port is serving **this** version: exits idempotently if it already is, restarts if the version is older (this is what the hooks call) |
49
+ | `agentacta --open` | Start the service and open the browser |
50
+ | `agentacta --client` | Start the floating desktop widget (first run downloads ~100MB of Electron runtime into `~/.agent-acta/widget-runtime/`) |
51
+ | `agentacta --stop` | Stop the service (graceful shutdown with index flush first; kills by pid only if there's no response) |
52
+ | `agentacta --status` | Status check: is the service running, does the version on the port match the local package, is the widget open (exit code 0 = match / 1 = mismatch or unreachable / 2 = not running) |
53
+ | `agentacta --doctor` | Self-check that prints a **paste-ready report**: config parses, configured directories still exist, index shards intact and consistent, the port is running this version, page files present, current archive size (exit code 0 = no failures / 1 = failures). **Read-only** — changes neither data nor config |
54
+ | `agentacta --install-hooks [--dry] [--agent atomcode,claude]` | Write "auto-start on session launch" into atomcode / claude configs (idempotent, backs up before changing) |
55
+ | `agentacta --uninstall-hooks` | Remove the hooks written above |
56
+ | `agentacta --where` | Print its own install path + a copy-paste-ready hook command |
57
+ | `agentacta --archive` | Archive once: freeze finished log entries older than today, with detail snapshots, into `~/.agent-acta/archive/` (some agents delete their own logs — if you don't freeze them they're gone forever). A running service also archives daily on its own; this command is for manual catch-up/troubleshooting |
58
+ | `agentacta --archive --list` | List the archive without archiving |
59
+ | `agentacta --archive --dry` | Report what would be written without touching disk (can combine with `--list`) |
60
+ | `agentacta --search-reindex [--dry]` | Manually rebuild the full-text search index (what the page search box queries when you press **Enter**). Normally unneeded: the service builds the first pass 60s after startup, then runs incremental passes every 10 minutes. **Refuses to run while the service is up** (`--stop` first); `--dry` only counts |
61
+ | `agentacta-mcp` | Run as an **MCP server** (stdio): a read-only tool for clients like Claude / Cursor / Qoder — see the next section |
62
+ | `agentacta --version` / `-v` | Version + service-script fingerprint (to tell whether the port is running this build) |
63
+ | `agentacta --help` / `-h` | Usage |
64
+
65
+ Unknown arguments fail with exit code `2` instead of silently starting the service.
66
+
67
+ **After editing server code**: `agentacta --ensure` is the one step (it compares code fingerprints and restarts when they differ). Page-only edits just need a browser refresh.
68
+
69
+ ## As an MCP server (let agents query their own logs)
70
+
71
+ `agentacta-mcp` is the second command shipped in the package; it exposes this machine's logs to clients as MCP tools. It is an **independent process that reads logs and indexes directly** — it does **not** connect to port 14570 and writes nothing, so it works whether or not the long-running service is up (after the initial handshake it does a full scan of the machine's logs, ~2s, logging nothing meanwhile).
72
+
73
+ Config snippet (replace the path with your actual install — `agentacta --where` prints it):
74
+
75
+ ```json
76
+ { "mcpServers": { "agent-acta": { "command": "node", "args": ["<install dir>/mcp-server.mjs"] } } }
77
+ ```
78
+
79
+ | Tool | What it does |
80
+ |---|---|
81
+ | `overview` | Which agents exist, how many entries each, index freshness |
82
+ | `search_entries` | List turns by agent/project/time range (newest first, 50 by default) |
83
+ | `get_entry` | Full text of one entry by id (prompt, output, tool calls, LLM detail) |
84
+ | `list_sessions` | Grouped by session: how many turns, how many tokens, over what span |
85
+ | `usage_stats` | `by=day` daily / `by=model` per-model aggregates |
86
+ | `slowest_turns` | Slowest N turns + p50/p95 |
87
+ | `tool_fails` | Which tools fail / time out most (three-way ranking + per-agent coverage; timeouts count as failures, soft failures listed separately) |
88
+
89
+ Tool semantics come from the same aggregation functions as the page/HTTP API — not a separate implementation. Full docs and per-client configs are in the "As an MCP server" section of REFERENCE (Chinese-only for now).
90
+
91
+ ## As a DSH plugin (the same panel inside DeepSeek Harness)
92
+
93
+ If you have [DeepSeek Harness](https://atomgit.com) (DSH) installed, you can install it as a Cordis plugin: an "AgentActa" icon appears in the left rail, opening a full panel in the center — the same Vue3 page (served in an iframe, with filters / details / SSE live push), **no extra browser tab or long-running service needed**.
94
+
95
+ ```
96
+ dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm package name
97
+ dsh plugin --profile desktop add "git+https://github.com/elegant01/agent-acta.git#v2.14.7" # or straight from the repo
98
+ ```
99
+
100
+ > The `@yxzpro/` scope exists because of npm's similarity rules: the bare `agent-acta` was rejected as too close to the existing `agentacta` (someone else's similar project, published 2026-02).
101
+ > **The product name is unchanged** — the command is still `agentacta`, and the repo, panel title and URL are all the same; only the npm identifier and DSH's bundle name follow the package name.
102
+
103
+ - `dsh plugin` forwards its argument verbatim to pnpm and accepts registry / git / tarball / path forms; for a plain file use `"file:C:/absolute/path/yxzpro-agent-acta-2.14.7.tgz"` instead (tarballs packed from a scoped package drop the `@` and turn `/` into `-`). ⚠ In Git Bash / PowerShell, **quote the whole argument** when the URL contains `#` or the package name contains `@`. Replace `v2.14.7` with the version you want (`git ls-remote --tags https://github.com/elegant01/agent-acta` lists them all).
104
+ - This project is released under the **MIT** license (see `LICENSE`); the code has no network egress and uploads no log content — it only reads files your agents already wrote to your own disk.
105
+ - **Fully quit DSH and start it again after installing** (all processes must exit — closing the window is not enough; the host has a module cache).
106
+ - If the icon doesn't show up, check these two first: ① the host gates plugin compatibility by semver (a mismatch just lands in `skippedBundles`, **no error** — the symptom is exactly "doesn't appear"); ② whether `--profile` points at the profile you actually use. Verified on **DSH 0.1.7-rc.2 / runtime 0.2.0-rc.1**.
107
+
108
+ **How it coexists with the CLI service** (the most-asked question): the plugin probes `127.0.0.1:14570` **on every request** (a ~1ms loopback probe, cached for 2s), so you don't have to choose:
109
+
110
+ | Port 14570 | What the panel gives you |
111
+ |---|---|
112
+ | Up | **Exactly that service's panel**, passed through in an iframe (data, SSE, write operations all work) — there is still only that one scanner on the machine, the plugin doesn't start another |
113
+ | Down | The plugin runs its own copy inside the host process (no port, no pid file, no idle self-stop) — the panel works as usual |
114
+
115
+ If the CLI exits mid-session, the panel switches to a small "CLI service has exited" page with a **let the plugin take over** button — it only starts when you click, never automatically: auto-starting could collide with you bringing the CLI back, creating two writers, and that decision belongs to a human.
116
+ You do **not** need to `agentacta --stop` or restart DSH just to view the panel (an earlier version made you do that — it was a bad experience and has been fixed).
117
+
118
+ **Boundaries** (deliberate, not omissions): the plugin doesn't launch the floating widget; the five routes `/` (the host's auth fallback slot), `/api/shutdown`, `/api/client`, `/api/trae/capture-key`, `/widget` are simply not registered in the host (so one misclick can't take the host process down); the panel keeps its own dark skin and doesn't follow the host's light/dark switching; the data path remains read-only-local with zero network egress. Details are in the "As a DSH plugin" section of REFERENCE.
119
+
120
+ ## Page usage cheat sheet
121
+
122
+ Open <http://127.0.0.1:14570>:
123
+
124
+ - **Filters**: agent / project / time range (today / last 7 days / last 30 days, by local calendar day) / status / keyword
125
+ - **Full-text search**: **typing** in the search box = filter the current window only (instant, free); **pressing Enter** = search the whole store — it searches the **body** of each turn
126
+ (user input + AI reply + tool args/results), across agents and across history not currently loaded, with **no time range applied by default** ("that error last week" is outside the default two days anyway).
127
+ Results take over the card list with hit snippets highlighted; the top banner honestly reports "indexed A of B entries" and "no time range applied", and whether this round was cut off by the
128
+ 200-per-query cap ("only 200 returned this time"); tick "restrict to current time range" to narrow by the selected dates.
129
+ The index is maintained incrementally in the background by the service (`~/.agent-acta/search/`); searchable about a minute after first launch
130
+ - **Cards**: click any card to expand user input, AI output, execution chain, LLM call detail and tool calls
131
+ - **Status badge top-right** = SSE connection state; click it to tune the refresh rate; the dropdown next to it enables "idle self-stop" (30 min / 2 h / 6 h, off by default)
132
+ - **Browse by session** (sidebar item 2): view full multi-turn evolution per session; dsh / zcode / trae sessions come with real timelines.
133
+ Session names come from **the product's own titles** (I14: claude-family reads `ai-title` from transcripts, falling back to `last-prompt`; dsh / gemini /
134
+ buddy / zcode / traedb / opencode / kilo / hermes / devin each have their own source); only when none exists does it fall back to the session key — **file names are never passed off as titles**
135
+ - **Sub-agent topology** (I16): sub-agent sessions indent with a `└` in the session list, and the parent's row shows "N turns total · x tok" (including itself);
136
+ the top of the right pane has clickable **jump to parent / jump to children**. The criterion is an explicit lineage key from the source (dsh's `parentSession`, claude's `subagents/` directory +
137
+ `.meta.json`, zcode/opencode's `session.parent_id`, hermes' `parent_session_id`) — **never guessed from time windows**
138
+ - **Unknown times are labeled honestly** (I16): turns whose timestamps can't be recovered (atomcode `.jsonl` files of 0 bytes / missing `turn_id`, 44.6% on this machine)
139
+ no longer impersonate the session's `updated_at` — the card says "time unknown", and they stay out of per-day statistics (the stats dialog says how many turns were left out)
140
+ - **Usage stats** (the line-chart icon in the toolbar): per-day token & latency aggregates + per-model, following the current filters;
141
+ the "latency analysis" dialog has four ranking sub-tabs — slowest turns / tool calls / cache hit rate / **tool-failure profile** (I6: which tools fail most,
142
+ with a coverage table: which sources have no per-tool failure signal at all — blank there doesn't mean "never fails")
143
+ - **Export** (download icon in the toolbar → JSON / CSV / Excel): exports exactly the currently filtered list set, and **writes the filter conditions into the file**
144
+ (first two rows of CSV, a note row in Excel, `meta.filters` in JSON) — so you can later prove "this batch was filtered by what". With a turn expanded,
145
+ the detail footer also offers **export this turn's full detail** (via `/api/entry?full=1`; tool args/results are not truncated). Pure browser download; the server writes nothing to disk;
146
+ semantics, column lists and "why Excel is SpreadsheetML" are in the "Export (R11)" section of REFERENCE.md
147
+ - **Two-turn compare** (the "compare" button on card top rows; click two cards and it opens automatically): side-by-side latency / model / LLM calls / tools / credits-or-tokens /
148
+ context usage / compaction, each metric marked with "which is bigger" and by how much; the lower half is a field-by-field view of both turns' user input, AI output and tool calls (fetched on open).
149
+ qoder only provides credits and percentages (it doesn't write tokens to disk, and printing 0 would read as "costs nothing"), so in mixed comparisons inapplicable cells show `—` and no diff is computed
150
+ - **Single-turn reproduction bundle** (the "export repro bundle" button in the card detail footer): one-click zip = that turn's **raw log segment** (cut with the parser's own turn-boundary rules,
151
+ line numbers match disk) + the full parse result + four version fingerprints + the filter conditions at export time — attach it to a group chat / bug report as-is.
152
+ The zip is hand-assembled in the browser (store mode, zero dependencies); **the server writes nothing and gains no network egress**; for the database / multi-frame-compressed sources
153
+ where raw segments can't be cut, it falls back to "original path + full parse result" and says why
154
+ - **History archive** (sidebar item 4): browse the frozen entries in `~/.agent-acta/archive/` — after a product deletes its own logs (CodeArts keeps only ~30 days), the user input / AI output / tool-call detail from back then still opens here. **Read-only side path**, not affected by the left-side filters; bottom-left also has a row of **per-agent archive switches** (turn off the ones you don't want to keep)
155
+ - **Disk usage** (sidebar item 6): per-agent breakdown of "log directory / archive / index shards" and which is biggest — see the composition before cleaning (top level of `~/.agent-acta` listed item by item, including the widget's Electron runtime). **Counts only, never deletes**; traversal runs with a budget, and if it can't finish it says "partial count" instead of showing a fake complete number
156
+ - **Parser self-test** (the sidebar item of the same name): after changing parsing semantics, see how the last regression run went — each script under `test/` with its result (green / red / timeout), duration, and **which ones were skipped this time and why**, all on one page. Data comes from `~/.agent-acta/selftest.json` (written by `node test/selftest.mjs`); this page is **read-only and runs no tests in the browser**; when the results came from a different build, a yellow bar at the top warns "this green doesn't represent the current build"
157
+ - The switch next to each agent in the sidebar = temporarily disable (config kept); `✕` = remove that agent's config (local log files untouched); `+` = add manually
158
+ - **The icon button on the cursor row = install/uninstall the token-capture hook**: cursor transcripts contain no tokens; the only local per-turn usage
159
+ comes from its own `stop` hook. One click (with confirmation) adds a command to `~/.cursor/hooks.json`, after which each turn's
160
+ input/output/cache usage is captured into `~/.agent-acta/cursor-usage.jsonl` and attached back onto the cards; click again to uninstall.
161
+ **It only adds its own entry and never touches your existing hooks**; if `hooks.json` isn't valid JSON it refuses rather than overwriting. Semantics in
162
+ the cursor section of [LOGFORMATS.md](LOGFORMATS.md), API in `POST /api/cursor-hook` in [REFERENCE.md](REFERENCE.md)
163
+ - Desktop widget (the lightweight, no-Electron way):
164
+ `msedge --app=http://127.0.0.1:14570/widget`, then pin on top with PowerToys
165
+
166
+ ## Data & configuration
167
+
168
+ - Config: `~/.agent-acta/config.json` (old locations `~/.agent-log/`, `~/.atomcode/agent-log/` are migrated automatically at startup)
169
+ - Index: `~/.agent-acta/index/<kind>.json` (incremental offsets persisted; restarts don't rescan everything)
170
+ - Archive: `~/.agent-acta/archive/<agent>/<YYYY-MM-DD>.jsonl` + `manifest.json` (entry-level freezing, kept forever by default)
171
+ - **Per-agent on/off switch**: click directly in the bottom-left of the "History archive" dialog (writes `archive.skip` in `config.json`, effective immediately, no restart);
172
+ editing the config by hand works too: `"archive": { "skip": ["cursor"] }`. **Only stops future archiving; already-frozen files are kept**
173
+ - The same section also supports `enabled` (disable automatic passes), `maxDays` / `maxMB` (caps; deletes whole-day files only). Details in the "History archive" section of REFERENCE
174
+ - Full-text index: `~/.agent-acta/search/<kind>.json` (the body of each turn, one record per source). **Maintained incrementally in the background by the service itself**
175
+ (first pass 60s after startup, then every 10 minutes); no manual intervention needed; to rebuild manually use `agentacta --search-reindex` (run `--stop` first if the service is up).
176
+ Real-machine reference numbers (evening of 2026-09-22, measured via `/api/search/status` on the long-running service): **648 sources / 5111 searchable entries** (that machine has 5431 in total;
177
+ the rest either had their source files deleted by the product or had no body in that turn to begin with) → index about **24MB**.
178
+ To lift the "index at most 8000 chars per body" cap, use the `AGENT_LOG_SEARCH_MAX_CHARS` environment variable
179
+ - Records recognize sources by **absolute path**, so after moving `~/.agent-acta` to another machine/platform, records that don't match
180
+ this machine are pruned and rescanned on load (both the log and `/api/search/status.pruned` report the numbers). On POSIX, `search/` `index/` `archive/`
181
+ are all created private (`0700`/`0600` — they contain plaintext conversations); existing files are left alone — to lock everything down at once, `chmod -R go-rwx ~/.agent-acta`
182
+ - Port: defaults to `14570`, overridable with `AGENT_LOG_PORT`
183
+ - pid file: `~/.agent-acta/server-<port>.pid`
184
+
185
+ ## Server HTTP API
186
+
187
+ | Endpoint | What it does |
188
+ |---|---|
189
+ | `GET /` | The page |
190
+ | `GET /widget` | Desktop widget page (SSE live refresh) |
191
+ | `GET /api/ping` | Liveness probe |
192
+ | `GET /api/version` | `{version, build, pid}`; build = content fingerprint of the service script |
193
+ | `GET /api/agents` · `POST /api/agents` · `DELETE /api/agents?name=` · `POST /api/agents/toggle` | CRUD for agent configs |
194
+ | `GET /api/diagnose` | Environment diagnosis: probe results and unrecognized reasons for every candidate path of every candidate agent |
195
+ | `GET /api/usage?force=1` | Disk usage: per-agent log/archive sizes + index shards + top-level composition of `~/.agent-acta` (results cached 60s; `force=1` bypasses) |
196
+ | `GET /api/selftest` | Results of the last regression self-test (passes `~/.agent-acta/selftest.json` through with three honest labels: `ageMs` / `sameBuild` / `sameParserRev`). **Runs no tests**; returns `ok:false` with a `hint` when never run |
197
+ | `GET /api/snapshot?limit&agent=&project=&range=&scan=1` | Entry snapshot (the list's main data) |
198
+ | `GET /api/daily` | Per-calendar-day token/latency aggregates (ignores limit; covers the whole filtered set) |
199
+ | `GET /api/models` | Per-model aggregates (same parameters) |
200
+ | `GET /api/analyze?range=&agent=&project=&from=&to=&top=` | Ranking analysis: `slowest` / `p50` / `p95` / `byTools` (tool calls per turn) / `byCacheRate` + `toolFails` (**I6 tool-failure profile**: `rows` gives `fail/total/rate` per agent×tool, `agents` gives coverage and "not indexed" counts). `top` caps at 50, default 10 |
201
+ | `GET /api/entry?id=&full=1` | Single-turn detail (dsh/zcode/traedb additionally return per-event `events[]`) |
202
+ | `GET /api/search?q=&re=1&agent=&project=&status=&from=&to=&limit=&offset=` | **Full-text search** (what the page search box hits on Enter). `from`/`to` are only sent when "restrict to current time range" is ticked — **omitted = no time limit**; each entry's `_snip` is the hit snippet (three plain-text segments; the page escapes them itself before highlighting). Empty `q` and invalid regexes get a 400 with the reason spelled out |
203
+ | `GET /api/search/status` | Full-text index coverage: `{entries, files, total, building, unreadable, noState, bytes, at, maxChars}` — the page banner "indexed A of B entries" uses the first four |
204
+ | `GET /api/sessions` · `GET /api/session?key=` | Session list and session detail ("browse by session") |
205
+ | `GET /api/events` | SSE: hello / update / remove / agents / settings / resync / scanning |
206
+ | `GET /api/settings` · `POST /api/settings` | Scan cadence / idle self-stop |
207
+ | `GET /api/ctxwindows` · `POST /api/ctxwindows` | Context-window table (the denominator of the claude/kimi progress bars) |
208
+ | `GET /api/archive/index` | Archive listing: `{root, days[], agents[], totals, skip[], configAgents[]}` (`skip` / `configAgents` feed the "archive switches" list) |
209
+ | `POST /api/archive/skip` | Per-agent archive switch: `{agent, skip}` → updates `config.archive.skip`, effective immediately |
210
+ | `GET /api/archive/entries?agent&day&q&limit&offset` | Archived entry list (**without** details, only a `hasDetail` flag) |
211
+ | `GET /api/archive/entry?agent&day&id` | One frozen detail snapshot (404 when gone) |
212
+ | `POST /api/shutdown` | Graceful shutdown (flush index, then exit) |
213
+
214
+ Per-endpoint field details and semantics are in the "Server HTTP API" section of [REFERENCE.md](REFERENCE.md).
215
+
216
+ ## Packaging & distribution (developers)
217
+
218
+ ```bash
219
+ mkdir -p ../dist
220
+ npm pack --pack-destination ../dist # → dist/agent-acta-<version>.tgz (bump package.json's version first)
221
+ node test/pack-smoke.mjs # smoke: unpack → run in an isolated env → probe every endpoint
222
+ ```
223
+
224
+ Regression/acceptance scripts live in `test/` (with fixtures, repeatable). Run them all and view results on the page:
225
+
226
+ ```bash
227
+ node test/selftest.mjs # default list (the ones that need nothing outside the repo), ~2 min serial
228
+ node test/selftest.mjs --only parser # only names matching (prefix or title contains the word)
229
+ node test/selftest.mjs --group parser # only one group: static / parser / cli / release / real / parity / slow
230
+ node test/selftest.mjs --all # including groups that are off by default (depends on the machine)
231
+ ```
232
+
233
+ Results are written to `~/.agent-acta/selftest.json` — that's what the sidebar "Parser self-test" page reads (details in the "Parser self-test" section of REFERENCE).
234
+
235
+ The distributed tgz **is also the DSH plugin package**: `files` includes `plugin/`, and the loading contract lives entirely in `package.json` — `exports["."]` lets the host dynamically import the server shell, `dsh.bundle.patch` points at `plugin/cordis.patch.yml`, `dsh.client.inject` lists the two host UI packages (miss them and it installs but the icon never appears). So after bumping the version, also check: `plugin/client.js` **must have no top-level `import`/`export`** (the host splices it verbatim into a plain `<script>`; ESM syntax there takes the whole DSH startup down).
236
+
237
+ ## Troubleshooting quick reference
238
+
239
+ - **No idea where to start**: `agentacta --doctor` — one pass listing the state of config / directories / indexes / port version / page files / archive; paste the whole output (**read-only**, changes nothing)
240
+ - **Page won't open / stuck on "reconnecting…"**: `curl http://127.0.0.1:14570/api/ping`; if there's no response, run `agentacta --ensure`
241
+ - **New endpoints 404 after upgrading/editing code**: the port is serving an old build — restart with `agentacta --ensure` (the criterion: the `build` fingerprint from `GET /api/version`; `--doctor` reports exactly this as `[fail]`)
242
+ - **An agent stays at 0 entries**: see the sidebar "Environment diagnosis" or `GET /api/diagnose`
243
+ - **Switching to mac / Linux (or another machine)**: just move `~/.agent-acta` over — indexes re-validate against this machine's disk and rebuild,
244
+ stale records prune themselves; **manually added** agent roots in `config.json` stay forever with 0 entries, and `--doctor` names them and hints
245
+ "these are Windows-shaped paths" — delete those entries and let auto-discovery re-detect. trae's key grabbing is Windows-only, so after switching
246
+ platforms its tokens and AI bodies are unavailable. Details in the "Cross-platform" section of [REFERENCE.md](REFERENCE.md)
247
+ - **trae shows no tokens / AI bodies**: the SQLCipher database needs a per-machine key; grab it with `tools/grab-trae-key.ps1` and fill in `traeKey` — steps in the "Trae's SQLCipher database" section of [REFERENCE.md](REFERENCE.md)
248
+ - More troubleshooting (codex user input empty, atomcode datalog, hook verification pipe stalls, source edits not taking effect, etc.) in the "FAQ" section of [REFERENCE.md](REFERENCE.md)
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # AgentActa
2
2
 
3
+ [English](./README.en.md) | 中文
4
+
3
5
  本地 AI agent 请求日志面板:聚合多款 AI agent 写在磁盘上的会话日志,在浏览器里查看每一「轮次/请求」的耗时、token、缓存命中、上下文占用与工具调用明细。
4
6
 
5
7
  - 本地常驻服务 + SSE 实时推送,**无外部依赖、无网络上报、纯只读旁路**(不注入、不改各 agent 的配置)
@@ -14,6 +16,15 @@
14
16
 
15
17
  ## 安装与快速上手
16
18
 
19
+ ### 从 npm 装
20
+
21
+ ```bash
22
+ npm i -g @yxzpro/agent-acta # 装完多出命令 agentacta 与 agentacta-mcp
23
+ agentacta --open # 起服务并打开 http://127.0.0.1:14570
24
+ ```
25
+
26
+ ⚠ 包名带作用域 `@yxzpro/`:裸名 `agent-acta` 与 npm 上已存在的 `agentacta`(别人的同类项目)被判定太像而发不出去。**命令名不受影响**,敲的还是 `agentacta`。国内镜像同步有先后(实测 npmmirror 还没同步到,腾讯云镜像已可回源),装不上就加 `--registry=https://registry.npmjs.org/`。
27
+
17
28
  ### 拿到的是压缩包(tgz)
18
29
 
19
30
  ```bash
@@ -85,27 +96,28 @@ node <本目录>/agent-acta-server.mjs --ensure # 或者不装 npm,直接
85
96
 
86
97
  ```
87
98
  dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm 包名
88
- dsh plugin --profile desktop add "git+https://atomgit.com/elegant001/agent-acta.git#v2.14.4" # 或直接从仓库
99
+ dsh plugin --profile desktop add "git+https://github.com/elegant01/agent-acta.git#v2.14.7" # 或直接从仓库
89
100
  ```
90
101
 
91
102
  > 包名带 `@yxzpro/` 是因为 npm 的相似性规则:裸名 `agent-acta` 与已存在的 `agentacta`(别人的同类项目,2026-02 就发了)判为太像而被拒。
92
103
  > **产品名没改** —— 命令仍是 `agentacta`,仓库、面板标题、URL 全不变;只有 npm 标识与 DSH 的 bundle 名跟着包名走。
93
104
 
94
- - `dsh plugin` 的参数逐字转发给 pnpm,认 registry / git / tarball / path 四态;只想要文件就把上面换成 `"file:C:/绝对路径/yxzpro-agent-acta-2.14.4.tgz"`(作用域包打出来的 tgz 文件名会去掉 `@`、把 `/` 变成 `-`)。⚠ `github:` 那种缩写认的是 GitHub,本仓库在 AtomGit 要写全 `git+https`;URL 里的 `#` 与带 `@` 的包名在 Git Bash / PowerShell 里**整条加引号**。把 `v2.14.4` 换成你要的版本即可(`git ls-remote --tags https://atomgit.com/elegant001/agent-acta.git` 能看全)。
105
+ - `dsh plugin` 的参数逐字转发给 pnpm,认 registry / git / tarball / path 四态;只想要文件就把上面换成 `"file:C:/绝对路径/yxzpro-agent-acta-2.14.7.tgz"`(作用域包打出来的 tgz 文件名会去掉 `@`、把 `/` 变成 `-`)。⚠ URL 里的 `#` 与带 `@` 的包名在 Git Bash / PowerShell 里**整条加引号**。把 `v2.14.7` 换成你要的版本即可(`git ls-remote --tags https://github.com/elegant01/agent-acta` 能看全)。
95
106
  - 本项目以 **MIT** 许可发布(见 `LICENSE`),代码里没有网络出口、也不上传任何日志内容 —— 它只读你自己机器上那些 agent 已经落盘的文件。
96
107
  - **装完要彻底退出 DSH 再启动**(进程全没才算,只关窗口无效 —— 宿主有模块缓存)。
97
108
  - 图标没出现,先按这两条查:① 宿主按 semver 判插件兼容(不匹配只进 `skippedBundles`、**不报错**,表现就是不出现);② `--profile` 有没有指对你实际在用的那个 profile。已在 **DSH 0.1.7-rc.2 / runtime 0.2.0-rc.1** 上验通。
98
109
 
99
- **和命令行服务怎么共处**(这条最常问):插件装载那一刻会探一次 `127.0.0.1:14570`。
110
+ **和命令行服务怎么共处**(这条最常问):插件**每次请求**现判 `127.0.0.1:14570` 在不在(环回探测约 1ms,结果缓存 2 秒),所以不用你选:
100
111
 
101
112
  | 14570 | 面板给你什么 |
102
113
  |---|---|
103
- | 在跑 | 一张说明页,写明「命令行那版服务正在跑,插件没有另起自己的扫描器」—— 因为两个载体共用同一个数据目录,两套扫描/归档同时写会互相抢 |
104
- | 没在跑 | 插件自己在宿主进程里起一套(不监听端口、不写 pid、不空闲自停),面板直接可用 |
114
+ | 在跑 | **就是它的面板**,原样递进 iframe(数据、SSE、写操作全通)—— 本机仍然只有它那一个扫描器,插件不另起一套 |
115
+ | 没在跑 | 插件自己在宿主进程里起一套(不监听端口、不写 pid、不空闲自停),面板照常用 |
105
116
 
106
- 想让插件接手:`agentacta --stop` → **重启 DSH**(分支是装载时定死的,只 `--stop` 不重开,面板会停在说明页不动,那不是坏)。反过来想回浏览器/命令行用:`agentacta --ensure` 就回来。
117
+ CLI 中途退出时面板会换成一小页「命令行服务已退出」,带一个**让插件接手**按钮 —— 点了才起,不自动起:自动起可能和你又把 CLI 拉回来撞成两个写者,这种决定留给人做。
118
+ **不需要**为了看面板去 `agentacta --stop` 或重启整个 DSH(早先那版要你这么做,体验很差,已改掉)。
107
119
 
108
- **边界**(有意为之,不是漏做):插件不唤起悬浮卡片,`/api/shutdown`、`/api/client`、`/api/trae/capture-key`、`/widget` 这四条路由在宿主里根本不注册(防一次误触把宿主进程带走);面板保持自家深色皮、不跟宿主明暗切换;数据面照旧只读本机、零网络出口。细节在 REFERENCE 的「作为 DSH 插件接入」。
120
+ **边界**(有意为之,不是漏做):插件不唤起悬浮卡片,`/`(宿主的认证兜底位)、`/api/shutdown`、`/api/client`、`/api/trae/capture-key`、`/widget` 这五条路由在宿主里根本不注册(防一次误触把宿主进程带走);面板保持自家深色皮、不跟宿主明暗切换;数据面照旧只读本机、零网络出口。细节在 REFERENCE 的「作为 DSH 插件接入」。
109
121
 
110
122
  ## 页面用法速查
111
123
 
package/REFERENCE.md CHANGED
@@ -1096,9 +1096,9 @@ stdout 一行一条 JSON-RPC 消息,stderr 是诊断。回归见 `test/mcp-tes
1096
1096
  ### 装与生效
1097
1097
 
1098
1098
  ```
1099
- dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm 包名(发布后可用)
1100
- dsh plugin --profile desktop add "git+https://atomgit.com/elegant001/agent-acta.git#v2.14.4"
1101
- dsh plugin --profile desktop add "file:C:/绝对路径/yxzpro-agent-acta-2.14.4.tgz" # 或直接给包
1099
+ dsh plugin --profile desktop add "@yxzpro/agent-acta" # npm 包名(已发布)
1100
+ dsh plugin --profile desktop add "git+https://github.com/elegant01/agent-acta.git#v2.14.7"
1101
+ dsh plugin --profile desktop add "file:C:/绝对路径/yxzpro-agent-acta-2.14.7.tgz" # 或直接给包
1102
1102
  dsh plugin --profile desktop list # 看装没装上
1103
1103
  ```
1104
1104
 
@@ -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
- 插件 `apply()` 时探一次 `127.0.0.1:14570`,**这个选择不会再变**(不重探):
1116
+ 插件对 `127.0.0.1:14570` 的探测是**每次请求**做的(环回探测约 1ms,结果缓存 2 秒),不是装载时定死:
1116
1117
 
1117
- | 探到 | 行为 |
1118
+ | 14570 | 行为 |
1118
1119
  |---|---|
1119
- | 有 CLI 服务在跑 | 只注册入口一条,面板显示说明页(含对方的 `version` / `build`),**不起第二套扫描器** —— 两个写者同抢 `~/.agent-acta` 的索引与归档是要出事的那种抢 |
1120
- | 没有 | `start({ mode:'hosted', listen:false })`:扫描 / 落索引 / SSE 心跳照跑,但**不监听端口、不写 pid、不挂空闲自停**,业务路由逐条注册进宿主的 `webServer` |
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
- 想从前者换到后者:`agentacta --stop` → 重启 DSH。反过来 `agentacta --ensure` 即可回浏览器用法。
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
 
@@ -102,7 +102,7 @@ import { claudeUserText, claudeIsToolResultOnly, claudeIsCommandText } from './p
102
102
  import { buddyUserText } from './parsers/buddy.mjs';
103
103
  import { mavisUserText } from './parsers/minimax.mjs';
104
104
  import { sniffBase, candidateBases, probeBases, kimiDesktopSessionsRoots, kimiWires, kimiWireIn, isKimiWire,
105
- buddyExtDataRoots,
105
+ buddyExtDataRoots, buddyExtMissingWhy,
106
106
  dshSessionFile, hasDshSessions, zcodeDbVerify, zcodeDbFile,
107
107
  opencodeDbFile, opencodeDbVerify,
108
108
  traeDbFile, traeLogKindOf, traeLogsFromDb,
@@ -857,6 +857,10 @@ async function runDoctor() {
857
857
  if (c.kind === 'dsh' && !DSH_ZSTD_OK)
858
858
  probs.push(['fail', 'dsh 的会话是 zstd 压缩的,当前 ' + process.version + ' 的 zlib 里没有 zstdDecompressSync(需 Node 22.15+)—— 升级 Node 即可,不是路径或格式问题']);
859
859
  const root = c.sessions || c.traces || '';
860
+ // codebuddy 名下有两份落盘(CLI + genie 扩展)。扩展那半没挂上时,病历上必须说得出**为什么**
861
+ // (没装扩展 / 没有 Data/ / <host> 名不认得 / 没有 history/ 四种成因),否则只能远程敲命令问盘。
862
+ // 问的是当前盘上的实情,不是配置里那份 sessionsExtra(它可能停在上一轮扫描的结论上)。
863
+ const bxWhy = (name === 'codebuddy' && !(c.sessionsExtra || []).length) ? buddyExtMissingWhy() : '';
860
864
  if (c.enabled === false) doc('info', 'agent ' + name, '已禁用,不参与扫描');
861
865
  else if (!confAvailable(c)) doc('warn', 'agent ' + name, '根不存在:' + (root || '(空)') +
862
866
  // 「换平台/换机器」是这句话最常见的来路:整个 ~/.agent-acta 搬过来,配置里的路径还是老平台的写法。
@@ -864,9 +868,11 @@ async function runDoctor() {
864
868
  (foreignPath(root) ? ' —— 这是 ' + (root.startsWith('/') ? 'mac / Linux' : 'Windows') + ' 的路径形态,而当前平台是 '
865
869
  + process.platform + ':像是一份从别的平台搬过来的配置。删掉它让自动发现重新认(或改成这台机器上的真实路径)'
866
870
  : '') +
867
- (c.source === 'auto' ? ' —— 下次启动服务时会自己摘掉' : ' —— 手工项会一直留着(确认是卸载了就删掉它)'));
871
+ (c.source === 'auto' ? ' —— 下次启动服务时会自己摘掉' : ' —— 手工项会一直留着(确认是卸载了就删掉它)') +
872
+ (bxWhy ? ';另:genie 扩展根也未探到 —— ' + bxWhy : ''));
868
873
  else doc('ok', 'agent ' + name, c.kind + ' · ' + root +
869
- (Array.isArray(c.sessionsExtra) && c.sessionsExtra.length ? '(另有 ' + c.sessionsExtra.length + ' 个额外根)' : ''));
874
+ (Array.isArray(c.sessionsExtra) && c.sessionsExtra.length ? '(另有 ' + c.sessionsExtra.length + ' 个额外根)'
875
+ : (bxWhy ? '(genie 扩展根未探到:' + bxWhy + ')' : '')));
870
876
  for (const [lv, t] of probs) doc(lv, 'agent ' + name, t);
871
877
  }
872
878
  }
package/core/service.mjs CHANGED
@@ -84,7 +84,7 @@ import { claudeUserText, claudeIsToolResultOnly, claudeIsCommandText } from '../
84
84
  import { buddyUserText } from '../parsers/buddy.mjs';
85
85
  import { mavisUserText } from '../parsers/minimax.mjs';
86
86
  import { sniffBase, candidateBases, probeBases, kimiDesktopSessionsRoots, kimiWires, kimiWireIn, isKimiWire,
87
- buddyExtDataRoots,
87
+ buddyExtDataRoots, buddyExtMissingWhy,
88
88
  dshSessionFile, hasDshSessions, zcodeDbVerify, zcodeDbFile,
89
89
  opencodeDbFile, opencodeDbVerify,
90
90
  traeDbFile, traeLogKindOf, traeLogsFromDb,
@@ -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
- let MODE = sameModule(path.resolve(process.argv[1] || ''), CLI_ENTRY) ? 'cli' : 'lib';
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),混进一行
@@ -804,6 +812,13 @@ function diagnose() {
804
812
  ? '声明式规则校验失败,已回落禁用(修好 config 里的 rules 后再手动启用):' +
805
813
  row.ruleErrors.map(e => e.rule + ' —— ' + e.msg).join(';')
806
814
  : '在页面上被手动关掉了,不参与扫描';
815
+ } else if (conf.missing === undefined) {
816
+ // conf.missing 只在扫描经过 agentScannable 时才会被赋值,所以「从没被赋过值」= 这一轮还没看过盘
817
+ // (首扫是分片的,一个 agent 一个 agent 过;或这条配置刚加进来)。这时候报「已接入 · 0 条」
818
+ // 再补一句「目录认出来了、也在扫,但一条都没解析出来」是假话 —— 它压根还没扫,
819
+ // 而 0 条只是还没轮到。首扫那几秒里整页都长这样,会把人送去查解析器。
820
+ row.state = 'pending'; row.stateText = '还没扫到';
821
+ row.note = '首扫是分片的,这一轮还没轮到它(或这条配置刚加进来)—— 现在 0 条不代表盘上没有,扫完这行会自己改口';
807
822
  } else if (row.missing) {
808
823
  row.state = 'missing'; row.stateText = '目录消失'; row.note = '配置里的目录(' + (row.sessions || row.traces) + ')现在不存在';
809
824
  } else if (!row.entries) {
@@ -873,6 +888,13 @@ function diagnose() {
873
888
  row.note = (row.note ? row.note + ' ' : '') + '⚠ 这个根同时也由「' + row.ownedBy + '」在扫:条目归属会被后扫的一方抢走(不是两份数据),两条里删掉一条即可';
874
889
  row.state = 'warn'; row.stateText = '重复的根';
875
890
  }
891
+ // codebuddy 名下有两份落盘。扩展那半没探到时,这一行看着只是「CLI 的数」,而成因有四种
892
+ // (没装扩展 / 没有 Data/ / <host> 名不认得 / 没有 history/)—— 不写出来的话,同事一句
893
+ // 「读不到 codebuddy」只能远程让人敲命令问盘。措辞与 [discover] 那行同源(buddyExtMissingWhy)。
894
+ if (name === 'codebuddy' && !(row.sessionsExtra || []).length) {
895
+ const bxWhy = buddyExtMissingWhy();
896
+ if (bxWhy) row.note = (row.note ? row.note + ' ' : '') + '(genie 扩展根未探到:' + bxWhy + ')';
897
+ }
876
898
  return row;
877
899
  };
878
900
  for (const name of KNOWN_AGENTS) {
@@ -1134,6 +1156,8 @@ function ownConfFor(name) {
1134
1156
  return cands[0] || null;
1135
1157
  }
1136
1158
 
1159
+ // codebuddy 扩展根的结论只记一份:discoverAgents 每轮扫描都会跑,日志要「变了才说一句」
1160
+ let bxWhyLogged = null;
1137
1161
  function discoverAgents() {
1138
1162
  for (const name of KNOWN_AGENTS) {
1139
1163
  const cur = agentConfs.get(name);
@@ -1255,14 +1279,20 @@ function discoverAgents() {
1255
1279
  // ⚠️ 与 trae 那种「两个源覆盖同一批会话」不同,这里两份是**互不重叠的会话**(CLI 用 uuid v7、
1256
1280
  // 扩展用 md5 串,会话 id 形状都不一样),所以不需要 purgeTraeOtherSide 那样的去重。
1257
1281
  // 每轮重算:扩展卸载了 / 数据根被挪走,死路径自己掉出去。
1258
- const bxRoots = buddyExtDataRoots().filter(r => !dirTaken({ sessions: r, traces: null }, 'codebuddy'));
1282
+ const bxAll = buddyExtDataRoots();
1283
+ const bxRoots = bxAll.filter(r => !dirTaken({ sessions: r, traces: null }, 'codebuddy'));
1259
1284
  const cb = agentConfs.get('codebuddy');
1260
1285
  if (cb && (cb.sessionsExtra || []).join('\n') !== bxRoots.join('\n')) {
1261
1286
  purgeAgent('codebuddy');
1262
1287
  cb.sessionsExtra = bxRoots;
1263
1288
  saveConfig();
1264
- log('[discover] codebuddy 扩展根:', bxRoots.join(' | ') || '(这台机器上没有)');
1265
1289
  }
1290
+ // 「扩展根没挂上」原来在日志里只有一句「这台机器上没有」,等于什么都没说 —— 成因有四种(没装扩展 /
1291
+ // 没有 Data/ / <host> 名不认得 / 没有 history/,明细见 buddyExtWhyMissing)。每轮都算一次,
1292
+ // 但只在结论变了时打一行:roots 为空是稳态,不能每轮刷。
1293
+ const bxWhy = bxRoots.length ? bxRoots.join(' | ')
1294
+ : '(没有:' + (bxAll.length ? '候选根已被别的 agent 占了' : (buddyExtMissingWhy() || '未知原因')) + ')';
1295
+ if (bxWhy !== bxWhyLogged) { bxWhyLogged = bxWhy; log('[discover] codebuddy 扩展根:', bxWhy); }
1266
1296
  }
1267
1297
 
1268
1298
  // 给「没认出来」的路径配一句**原因**。
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@yxzpro/agent-acta",
3
- "version": "2.14.4",
3
+ "version": "2.14.8",
4
4
  "author": "elegant001",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://atomgit.com/elegant001/agent-acta.git"
7
+ "url": "git+https://github.com/elegant01/agent-acta.git"
8
8
  },
9
- "homepage": "https://atomgit.com/elegant001/agent-acta#readme",
9
+ "homepage": "https://github.com/elegant01/agent-acta#readme",
10
10
  "description": "本地 AI agent 请求日志面板:聚合 atomcode / codebuddy / workbuddy / claude / codex / codearts / gemini / cursor / kimi / dsh 等本地日志,看每轮耗时、token、缓存、上下文与工具调用",
11
11
  "bin": {
12
12
  "agentacta": "./agent-acta-server.mjs",
@@ -419,6 +419,38 @@ export function buddyExtRoot(base) {
419
419
  return null;
420
420
  }
421
421
 
422
+ // ---------------- 「扩展根为什么没探到」的病历(诊断 / --doctor 用,不参与判据)----------------
423
+ // buddyExtRoot 返回 null 有四种成因,而在页面上长得一模一样(codebuddy 那行只剩 CLI 的数):
424
+ // ① 这台机器没有 CodeBuddyExtension 目录;② 有目录但没有 Data/;
425
+ // ③ 有 Data,但 <acct> 底下的 <host> 名不在 HOSTS 白名单里(新版换宿主名就会这样,最隐蔽);
426
+ // ④ host 名对得上,但底下没有 history/。
427
+ // 不分开的话,同事一句「读不到 codebuddy」只能远程敲命令问盘 —— 这里把**实际看到的目录名**带回来。
428
+ // ⚠️ 只在用户点开诊断页 / 跑 --doctor 时调;同样只用 listDirCached,别挂到 3s 扫描路径上。
429
+ export function buddyExtWhyMissing(base) {
430
+ const dirs = d => (listDirCached(d) || []).filter(n => isDir(path.join(d, n)));
431
+ const cap = (arr, n = 6) => (arr.length > n ? arr.slice(0, n).concat('…(共 ' + arr.length + ' 个)') : arr);
432
+ if (!isDir(base)) return '目录不存在';
433
+ const data = path.join(base, 'Data');
434
+ if (!isDir(data)) return '下面没有 Data/(实际子目录:' + (cap(dirs(base)).join(' ') || '空') + ')';
435
+ const accts = dirs(data);
436
+ if (!accts.length) return 'Data/ 是空的(一个账号目录都没有)';
437
+ const badHosts = [], noHist = [];
438
+ for (const acct of accts.slice(0, 8)) {
439
+ const hosts = dirs(path.join(data, acct));
440
+ const known = hosts.filter(h => HOSTS.has(h.toLowerCase()));
441
+ if (!known.length) { badHosts.push(acct + '/ 下是「' + (hosts.join(' ') || '空') + '」'); continue; }
442
+ for (const h of known) {
443
+ const hd = path.join(data, acct, h);
444
+ if (!isDir(path.join(hd, 'history')) && !dirs(hd).some(m => isDir(path.join(hd, m, 'history')))) {
445
+ noHist.push(acct + '/' + h);
446
+ }
447
+ }
448
+ }
449
+ if (badHosts.length) return cap(badHosts).join(';') + ' —— <host> 名都不是那三个宿主(只认 ' + [...HOSTS].join(' / ') + ')';
450
+ if (noHist.length) return '<host> 名认得、但底下没有 history/:' + cap(noHist).join(';');
451
+ return 'Data/ 下有账号目录,却没探到 history/(账号目录:' + cap(accts).join(' ') + ')';
452
+ }
453
+
422
454
  // ---------------- 扫描主入口 ----------------
423
455
  // root = <Data> 目录(由 sniffBase 解析出来)
424
456
  export function scanBuddyExt(agent, root, onlyFile) {
@@ -10,7 +10,7 @@ import { HOME, isDir, isFile, listDirCached, sqliteMod } from './shared.mjs';
10
10
  import { hasMavisSession } from './minimax.mjs';
11
11
  import { OPENCLAW_STATE_DIRNAMES, openclawTranscripts } from './openclaw.mjs';
12
12
  import { clineTranscripts } from './cline.mjs';
13
- import { buddyExtRoot } from './buddyext.mjs';
13
+ import { buddyExtRoot, buddyExtWhyMissing } from './buddyext.mjs';
14
14
  // hasCursorTranscripts 里枚举转录走的是 cursor 解析器那一个函数(两种落法共用一套判定,见该函数注释)。
15
15
  // ⚠️ 这条 import 曾经漏掉过:函数体在外层 try{}catch{} 里,ReferenceError 被静默吞掉 →
16
16
  // hasCursorTranscripts **恒 false** → 自动发现整条 cursor 分支永不命中(症状 = 侧栏 cursor 恒 0 条,
@@ -1224,6 +1224,18 @@ export function buddyExtDataRoots() {
1224
1224
  return out;
1225
1225
  }
1226
1226
 
1227
+ // 「codebuddy 的扩展根为什么没挂上」——一句能直接抄进病历的话(探到了就回空串,调用方据此不写)。
1228
+ // 三个候选根(~/、%APPDATA%、%LOCALAPPDATA%)在没装扩展的机器上**全都**不存在,那种情况合并成一句:
1229
+ // 三行「目录不存在」会把真正有用的那行(目录在、结构不对)淹掉。
1230
+ // c(可选,同 basesFor 的平台上下文)是给测试用的:不传就是当前这台机器。
1231
+ export function buddyExtMissingWhy(c) {
1232
+ const bases = basesFor(BUDDY_EXT_DIRNAMES, c);
1233
+ if (bases.some(b => buddyExtRoot(b))) return ''; // 探到了就不解释
1234
+ const present = [...new Set(bases.filter(b => isDir(b)))]; // 去重:APPDATA 与 LOCALAPPDATA 被指到同一个目录时别说两遍
1235
+ if (present.length) return present.map(b => b + ' —— ' + buddyExtWhyMissing(b)).join(';');
1236
+ return '这台机器没有 CodeBuddyExtension 目录(候选:' + bases.join('、') + ')—— 没装 genie 扩展,或它的数据根被挪走了';
1237
+ }
1238
+
1227
1239
  // re-export 让 sniffBase 能直接用 hasMavisSession(定义在 parsers/minimax.mjs):
1228
1240
  // 不在头部 import 是为避开「discovery 静态 import minimax.mjs → 服务启动必须先解析 minimax.mjs」
1229
1241
  // 的耦合;minimax.mjs 现在只 import shared.mjs,本身已是轻量模块,import 进来即可。
package/plugin/plugin.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  // AgentActa 的 DSH 插件壳(I11 批 2)。
2
2
  //
3
- // 形状是「URL 转接 + 逐条注册」,不是第二套服务:所有 handler 一律转给 core 的 handle(req,res),
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
- // 宿主里不该存在的四条:退进程 / 起 Electron 壳 / 弹 UAC 抓密钥 / 悬浮球壳。
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
- // 面板的唯一入口路径:两支(A 说明页 / hosted 递面板 HTML)都挂在这一条上,
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
- export const HOSTED_EXTRA = [ENTRY_PATH, ASSET_PATH];
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
- // 暂定 A(PLAN §8.8):老 CLI 服务在跑时**不另起一份扫描器**,面板如实说明并给出退出办法。
45
- // 选 B(代理转发)时换掉这一支即可 —— 所以它是个开关,不是一处重构。
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: PORT, timeout: 600 });
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
- const NOTICE = (cli) => `<!doctype html><html lang="zh-CN"><meta charset="utf-8">
151
- <title>AgentActa</title><style>body{font:14px/1.7 ui-monospace,Consolas,monospace;background:#12161d;color:#dfe6ef;padding:22px;max-width:46rem}
152
- code{background:#ffffff14;padding:2px 6px;border-radius:4px}</style>
153
- <body><h3>AgentActa 面板没起来 —— 命令行那版服务正在跑</h3>
154
- <p>端口 <code>127.0.0.1:${PORT}</code> 上有一个 AgentActa 服务(版本 <code>${cli && cli.version}</code> / 指纹 <code>${cli && cli.build}</code>)。
155
- 它和插件是同一套数据目录,两个扫描器同时跑会互相抢着写索引与归档,所以插件这次<b>没有起自己的服务</b>。</p>
156
- <p>想看面板:直接开 <a href="http://127.0.0.1:${PORT}/">http://127.0.0.1:${PORT}/</a>(就是它);
157
- 想让插件接手:先跑 <code>agentacta --stop</code>,再重开这个面板。</p></body></html>`;
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
- const cli = await cliProbe();
180
-
181
- if (cli) {
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
- await start({ mode: 'hosted', listen: false });
193
-
194
- const passthrough = (req, res) => handle(req, res);
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, passthrough);
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
- // 面板入口不再 302 到宿主 http 口:iframe 从 dsh-app:// 跳 http://127.0.0.1 是跨源,实测被拦(空白),
214
- // 而且那口上的 /page/、/vendor/ 在 iframe 里本来也到不了。改成由 ENTRY_PATH 自己把 HTML 递出来,
215
- // 页面里的静态引用由 rewriteRefs 翻译成 ASSET_PATH —— 全程同源、单源。
216
- webCtx.logger?.info?.('[agent-acta] hosted 就绪 v' + VERSION + ' (' + BUILD + '),路由 ' + registered.length + ' 条,入口 ' + ENTRY_PATH);
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
- web.effect(() => () => stop('DSH 插件卸载'));
298
+ // 只有真起了 hosted 才需要收尾停掉它(代理那一支没起任何东西)
299
+ web.effect(() => () => { if (local) stop('DSH 插件卸载'); });
219
300
  });
220
301
  }