@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.2
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/CHANGELOG.md +158 -0
- package/LICENSE +21 -0
- package/README.md +181 -2
- package/config/edge.example.json +179 -0
- package/config/privacy-policy.json +202 -0
- package/cordis.desktop-safe.patch.yml +129 -0
- package/cordis.patch.yml +382 -0
- package/cordis.yml +374 -0
- package/lib/attestation.d.ts +2 -0
- package/lib/attestation.d.ts.map +1 -0
- package/lib/attestation.js +2 -0
- package/lib/audit-log.d.ts +2 -0
- package/lib/audit-log.d.ts.map +1 -0
- package/lib/audit-log.js +2 -0
- package/lib/client.js +87 -0
- package/lib/cloud-llm-adapter.d.ts +2 -0
- package/lib/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/cloud-llm-adapter.js +2 -0
- package/lib/dsh-adapter/attestation.d.ts +42 -0
- package/lib/dsh-adapter/attestation.d.ts.map +1 -0
- package/lib/dsh-adapter/attestation.js +40 -0
- package/lib/dsh-adapter/audit-log.d.ts +139 -0
- package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
- package/lib/dsh-adapter/audit-log.js +376 -0
- package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
- package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-egress.js +257 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
- package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
- package/lib/dsh-adapter/contract.d.ts +80 -0
- package/lib/dsh-adapter/contract.d.ts.map +1 -0
- package/lib/dsh-adapter/contract.js +194 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
- package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
- package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
- package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
- package/lib/dsh-adapter/edge-config.d.ts +484 -0
- package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-config.js +898 -0
- package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
- package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
- package/lib/dsh-adapter/edge-context-view.js +483 -0
- package/lib/dsh-adapter/effective-model.d.ts +85 -0
- package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
- package/lib/dsh-adapter/effective-model.js +171 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
- package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
- package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
- package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
- package/lib/dsh-adapter/etscli-tools.js +393 -0
- package/lib/dsh-adapter/file-logger.d.ts +294 -0
- package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
- package/lib/dsh-adapter/file-logger.js +564 -0
- package/lib/dsh-adapter/function-call-external.d.ts +357 -0
- package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
- package/lib/dsh-adapter/function-call-external.js +1005 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
- package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/host-egress-guard.js +213 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
- package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/host-llm-trace.js +344 -0
- package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
- package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-routes.js +79 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
- package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api-sessions.js +173 -0
- package/lib/dsh-adapter/http-api.d.ts +221 -0
- package/lib/dsh-adapter/http-api.d.ts.map +1 -0
- package/lib/dsh-adapter/http-api.js +1811 -0
- package/lib/dsh-adapter/index.d.ts +62 -0
- package/lib/dsh-adapter/index.d.ts.map +1 -0
- package/lib/dsh-adapter/index.js +97 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
- package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard-messages.js +136 -0
- package/lib/dsh-adapter/input-guard.d.ts +508 -0
- package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/input-guard.js +2174 -0
- package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
- package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-prompts.js +97 -0
- package/lib/dsh-adapter/intent-router.d.ts +710 -0
- package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-router.js +2802 -0
- package/lib/dsh-adapter/intent-signals.d.ts +208 -0
- package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
- package/lib/dsh-adapter/intent-signals.js +391 -0
- package/lib/dsh-adapter/invariant-install.d.ts +16 -0
- package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant-install.js +15 -0
- package/lib/dsh-adapter/invariant.d.ts +16 -0
- package/lib/dsh-adapter/invariant.d.ts.map +1 -0
- package/lib/dsh-adapter/invariant.js +22 -0
- package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
- package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-adapters.js +16 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
- package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-internal-caller.js +100 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
- package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-max-turns.js +127 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
- package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace-store.js +205 -0
- package/lib/dsh-adapter/llm-trace.d.ts +450 -0
- package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
- package/lib/dsh-adapter/llm-trace.js +463 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
- package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
- package/lib/dsh-adapter/local-llm-adapter.js +787 -0
- package/lib/dsh-adapter/local-storage.d.ts +96 -0
- package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
- package/lib/dsh-adapter/local-storage.js +198 -0
- package/lib/dsh-adapter/model-router.d.ts +79 -0
- package/lib/dsh-adapter/model-router.d.ts.map +1 -0
- package/lib/dsh-adapter/model-router.js +107 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
- package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
- package/lib/dsh-adapter/ohttp-gateway.js +32 -0
- package/lib/dsh-adapter/openai-sse.d.ts +85 -0
- package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
- package/lib/dsh-adapter/openai-sse.js +462 -0
- package/lib/dsh-adapter/pii-utils.d.ts +96 -0
- package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
- package/lib/dsh-adapter/pii-utils.js +324 -0
- package/lib/dsh-adapter/plugin-host.d.ts +38 -0
- package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin-host.js +38 -0
- package/lib/dsh-adapter/plugin.d.ts +46 -0
- package/lib/dsh-adapter/plugin.d.ts.map +1 -0
- package/lib/dsh-adapter/plugin.js +100 -0
- package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
- package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
- package/lib/dsh-adapter/privacy-policy.js +391 -0
- package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
- package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
- package/lib/dsh-adapter/prompt-guard.js +872 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
- package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
- package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
- package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
- package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
- package/lib/dsh-adapter/secure-bridge.js +35 -0
- package/lib/dsh-adapter/session-attribution.d.ts +70 -0
- package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
- package/lib/dsh-adapter/session-attribution.js +76 -0
- package/lib/dsh-adapter/sessions.d.ts +86 -0
- package/lib/dsh-adapter/sessions.d.ts.map +1 -0
- package/lib/dsh-adapter/sessions.js +179 -0
- package/lib/dsh-adapter/tcm.d.ts +55 -0
- package/lib/dsh-adapter/tcm.d.ts.map +1 -0
- package/lib/dsh-adapter/tcm.js +94 -0
- package/lib/dsh-adapter/td-bus.d.ts +87 -0
- package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
- package/lib/dsh-adapter/td-bus.js +196 -0
- package/lib/dsh-adapter/tool-runner.d.ts +185 -0
- package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
- package/lib/dsh-adapter/tool-runner.js +271 -0
- package/lib/dsh-adapter/tools.d.ts +118 -0
- package/lib/dsh-adapter/tools.d.ts.map +1 -0
- package/lib/dsh-adapter/tools.js +292 -0
- package/lib/dsh-adapter/types.d.ts +23 -0
- package/lib/dsh-adapter/types.d.ts.map +1 -0
- package/lib/dsh-adapter/types.js +16 -0
- package/lib/dsh-adapter/voice-input.d.ts +30 -0
- package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
- package/lib/dsh-adapter/voice-input.js +34 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
- package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
- package/lib/http-api.d.ts +2 -0
- package/lib/http-api.d.ts.map +1 -0
- package/lib/http-api.js +2 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/intent-router.d.ts +2 -0
- package/lib/intent-router.d.ts.map +1 -0
- package/lib/intent-router.js +2 -0
- package/lib/llm-adapters.d.ts +2 -0
- package/lib/llm-adapters.d.ts.map +1 -0
- package/lib/llm-adapters.js +2 -0
- package/lib/local-llm-adapter.d.ts +2 -0
- package/lib/local-llm-adapter.d.ts.map +1 -0
- package/lib/local-llm-adapter.js +2 -0
- package/lib/local-storage.d.ts +2 -0
- package/lib/local-storage.d.ts.map +1 -0
- package/lib/local-storage.js +2 -0
- package/lib/model-router.d.ts +2 -0
- package/lib/model-router.d.ts.map +1 -0
- package/lib/model-router.js +2 -0
- package/lib/ohttp-gateway.d.ts +2 -0
- package/lib/ohttp-gateway.d.ts.map +1 -0
- package/lib/ohttp-gateway.js +2 -0
- package/lib/plugin-host.d.ts +2 -0
- package/lib/plugin-host.d.ts.map +1 -0
- package/lib/plugin-host.js +2 -0
- package/lib/privacy-policy.d.ts +2 -0
- package/lib/privacy-policy.d.ts.map +1 -0
- package/lib/privacy-policy.js +2 -0
- package/lib/prompt-guard.d.ts +2 -0
- package/lib/prompt-guard.d.ts.map +1 -0
- package/lib/prompt-guard.js +2 -0
- package/lib/sanitizer-pipeline.d.ts +2 -0
- package/lib/sanitizer-pipeline.d.ts.map +1 -0
- package/lib/sanitizer-pipeline.js +2 -0
- package/lib/secure-bridge.d.ts +2 -0
- package/lib/secure-bridge.d.ts.map +1 -0
- package/lib/secure-bridge.js +2 -0
- package/lib/sessions.d.ts +2 -0
- package/lib/sessions.d.ts.map +1 -0
- package/lib/sessions.js +2 -0
- package/lib/tcm.d.ts +2 -0
- package/lib/tcm.d.ts.map +1 -0
- package/lib/tcm.js +2 -0
- package/lib/td-bus.d.ts +2 -0
- package/lib/td-bus.d.ts.map +1 -0
- package/lib/td-bus.js +2 -0
- package/lib/tools.d.ts +2 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +2 -0
- package/lib/voice-input.d.ts +2 -0
- package/lib/voice-input.d.ts.map +1 -0
- package/lib/voice-input.js +2 -0
- package/package.json +253 -3
- package/presets/agent-preset-edge.json +50 -0
- package/presets/edge.desktop-safe.patch.yml +503 -0
- package/presets/edge.patch.yml +557 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-logger — M6 durable file exporter for Cordis `ctx.logger`.
|
|
3
|
+
*
|
|
4
|
+
* DSH host does not ship a service logger (`dsh --help` has no `--log-file`),
|
|
5
|
+
* and Cordis's LoggerService only installs an in-memory ring buffer (1000
|
|
6
|
+
* entries). Without an external exporter every `ctx.logger.info(...)` call
|
|
7
|
+
* lands in that buffer and is lost on exit. We register a file exporter so
|
|
8
|
+
* every dsh-edge-agent plugin's log output is durably persisted to disk,
|
|
9
|
+
* rotated on size, and tail-able for offline troubleshooting.
|
|
10
|
+
*
|
|
11
|
+
* Cordis `export()` contract is **strictly synchronous** (see
|
|
12
|
+
* `@deepseek-ai/cordis@4.0.1/src/logger.ts:46`; the loop at `:154-159`
|
|
13
|
+
* does not await) — the only safe pattern is to synchronously push the
|
|
14
|
+
* formatted line into an in-memory queue, then let a background
|
|
15
|
+
* `setInterval().unref()` flush the queue to disk. The audit-log plugin
|
|
16
|
+
* uses the same pattern (`audit-log.ts:147-152`).
|
|
17
|
+
*
|
|
18
|
+
* Loop avoidance: `export()` must NEVER call `ctx.logger.error(...)` —
|
|
19
|
+
* audit-log's own diagnostics already do (`audit-log.ts:266/309`), and a
|
|
20
|
+
* failure here would re-enter our own `export()` and spin. Failures are
|
|
21
|
+
* swallowed silently and counted; a stderr notice is emitted on the first
|
|
22
|
+
* drop and every 1000th drop after that.
|
|
23
|
+
*
|
|
24
|
+
* Append-mode (`fs.open(path, 'a')`) means each process boot appends to
|
|
25
|
+
* the existing file instead of truncating it, so install.sh's truncate-on-
|
|
26
|
+
* restart semantics are no longer in play — dsh-edge-agent's own writer
|
|
27
|
+
* owns the file lifecycle.
|
|
28
|
+
*
|
|
29
|
+
* ═══════════════════════════════════════════════════════════════════════
|
|
30
|
+
* OPERATIONS / AUDIT CHANNEL DISCIPLINE (2026-09-01)
|
|
31
|
+
* ═══════════════════════════════════════════════════════════════════════
|
|
32
|
+
* Two log channels. They serve different consumers. Do not blur them.
|
|
33
|
+
*
|
|
34
|
+
* ┌─ OPERATIONS LOG ──────────────────────────────────────────────────┐
|
|
35
|
+
* │ Path: ~/.dsh-edge-agent/log/dsh-edge-agent.log (this file) │
|
|
36
|
+
* │ Source: every `ctx.logger.{info,warn,error,debug}` call │
|
|
37
|
+
* │ Rule: **info / warn / error entries MUST NOT contain request │
|
|
38
|
+
* │ bodies, response bodies, tool arguments, prompt text, │
|
|
39
|
+
* │ or any user PII.** Only metadata (provider, model, │
|
|
40
|
+
* │ reqId, durationMs, counts) and error diagnostics. │
|
|
41
|
+
* │ │
|
|
42
|
+
* │ **debug entries MAY carry LLM call bodies** (request │
|
|
43
|
+
* │ messages + response text) for offline triage — opt-in │
|
|
44
|
+
* │ via `DSH_EDGE_LOG_LEVEL=debug`. 2026-09-11 default │
|
|
45
|
+
* │ flipped from `debug` to `info` (users said log was too │
|
|
46
|
+
* │ noisy — every LLM call landed 4 body lines). Re-enable │
|
|
47
|
+
* │ with `DSH_EDGE_LOG_LEVEL=debug`. CLOUD prompts are │
|
|
48
|
+
* │ pre-sanitized by input-guard before egress, so the │
|
|
49
|
+
* │ captured prompt is already placeholder-text for │
|
|
50
|
+
* │ sensitive spans. CLOUD response text is NOT redacted. │
|
|
51
|
+
* │ Call sites: cloud-llm-adapter.ts, local-llm-adapter.ts. │
|
|
52
|
+
* │ Used by operators to triage service flow, failures, and │
|
|
53
|
+
* │ LLM call replay. │
|
|
54
|
+
* └───────────────────────────────────────────────────────────────────┘
|
|
55
|
+
*
|
|
56
|
+
* ┌─ AUDIT LOG ───────────────────────────────────────────────────────┐
|
|
57
|
+
* │ Path: ~/.dsh-edge-agent/audit/audit.plain.jsonl │
|
|
58
|
+
* │ Source: every `auditLog.log({...})` call — explicit, opt-in │
|
|
59
|
+
* │ Rule: **Records the actual call content** (provider, model, │
|
|
60
|
+
* │ promptLen, messageCount, toolName, redacted args, │
|
|
61
|
+
* │ durationMs). Fields are PII-redacted via │
|
|
62
|
+
* │ `pii-utils.redactObject()` before write. Used by │
|
|
63
|
+
* │ compliance / legal / security review. │
|
|
64
|
+
* └───────────────────────────────────────────────────────────────────┘
|
|
65
|
+
*
|
|
66
|
+
* Concretely: every existing `ctx.logger.info(...)` call in dsh-edge-agent
|
|
67
|
+
* carries only metadata. **Before adding a new ctx.logger call**, confirm
|
|
68
|
+
* it does not embed any of: `messages`, `args` (tool args), `promptText`,
|
|
69
|
+
* response bodies, session content, or user-supplied strings. If you need
|
|
70
|
+
* to log that data, route it through `auditLog.log(...)` so it lands in
|
|
71
|
+
* the audit JSONL with field-level redaction, NOT here.
|
|
72
|
+
*
|
|
73
|
+
* Wiring (2026-09-01): file-logger is registered as a standalone Cordis
|
|
74
|
+
* plugin id (`profiles/dsh-edge-agent/cordis.patch.yml`) so its apply()
|
|
75
|
+
* runs at boot — before audit-log / http-api / etc. The disabled
|
|
76
|
+
* aggregator `dsh-edge-agent` (see cordis.patch.yml:26-29) means the
|
|
77
|
+
* internal `src/dsh-adapter/index.ts:apply()` is never invoked, so we
|
|
78
|
+
* can't rely on it to install the exporter. Each plugin id in the patch
|
|
79
|
+
* is loaded independently via its `name:` field.
|
|
80
|
+
*
|
|
81
|
+
* @module dsh-edge-agent/file-logger
|
|
82
|
+
*/
|
|
83
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
84
|
+
import { mkdir, open, stat } from 'node:fs/promises';
|
|
85
|
+
import { renameWithRetry } from './local-storage.js';
|
|
86
|
+
import { loadEdgeConfig } from './edge-config.js';
|
|
87
|
+
import { existsSync } from 'node:fs';
|
|
88
|
+
import { dirname, join } from 'node:path';
|
|
89
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
90
|
+
export const name = 'file-logger';
|
|
91
|
+
/**
|
|
92
|
+
* 无硬依赖(§7.169,2026-10-31)。
|
|
93
|
+
*
|
|
94
|
+
* 原来是 `inject = ['web']`,只为了一句 boot 诊断(web-diag)能读
|
|
95
|
+
* `ctx.web.searchProviders.size`。代价是**整个 ops log 通道被一个无关服务
|
|
96
|
+
* 卡住**:cordis 要等 `web` 服务就绪才让本插件 apply,而**注册文件导出器的
|
|
97
|
+
* 正是本插件** ⇒ 凡是比 file-logger 更早 apply 的插件,它的启动日志(乃至
|
|
98
|
+
* 拒绝行)会**静默丢失**。
|
|
99
|
+
*
|
|
100
|
+
* 实测(§7.169,headless `dsh-edge-agent` profile,探针实测):
|
|
101
|
+
* - 守卫 apply 那一刻 `ctx.logger` 还没有落盘导出器;
|
|
102
|
+
* - 同一 ctx 上 `ctx.logger.info(...)` 在 t0 **丢失**,t300 / t1500 / t4000
|
|
103
|
+
* 才落盘 —— 即「闸门上膛」启动行与拒绝行都可能无处可落。
|
|
104
|
+
*
|
|
105
|
+
* 改法:`web` 从硬依赖降为**延迟注入**(`ctx.inject(['web'], cb)`,与
|
|
106
|
+
* edge-context-view / input-guard 同款纪律),本插件零静态依赖 ⇒ 导出器注册
|
|
107
|
+
* 不再被无关服务拖后。**注意**:实测同层插件的 apply 先后由模块解析速度
|
|
108
|
+
* 决定、不靠配置树顺序(§7.169),所以「通道就位」不能靠"谁先 apply"保证 ——
|
|
109
|
+
* 消费方要用 `ctx.inject(['fileLogger'], …)` 延迟注入,回调触发时导出器必已
|
|
110
|
+
* 注册(cordis `_updateState` 只在 providing fiber 变 ACTIVE 后 notify)。
|
|
111
|
+
*
|
|
112
|
+
* 注意:桌面版 ops log 的文件名(`dsh-edge-agent-desktop.log`)来自**行**
|
|
113
|
+
* 的 config,本插件行必须比裸包名聚合行先装载 —— 零依赖后这个顺序只会更稳。
|
|
114
|
+
*/
|
|
115
|
+
export const inject = [];
|
|
116
|
+
/** Numeric Cordis logger level (mirrors @deepseek-ai/cordis/src/logger.ts:22-27). */
|
|
117
|
+
const LEVEL_NUMERIC = {
|
|
118
|
+
error: 0,
|
|
119
|
+
info: 1,
|
|
120
|
+
warn: 2,
|
|
121
|
+
debug: 3,
|
|
122
|
+
};
|
|
123
|
+
/** Reverse lookup for `resolveLoggerConfig` env validation. */
|
|
124
|
+
function parseLogLevel(value, fallback = 'info') {
|
|
125
|
+
if (value === 'debug' || value === 'info' || value === 'warn' || value === 'error')
|
|
126
|
+
return value;
|
|
127
|
+
return fallback;
|
|
128
|
+
}
|
|
129
|
+
// §7.102 "one config file": root/level/maxFileAgeDays/maxRotatedFiles carry
|
|
130
|
+
// NO Schema default — resolveLoggerConfig()/apply() fill them from
|
|
131
|
+
// config/edge.json (audit.*) with the env params as per-process overrides.
|
|
132
|
+
// (The old Schema default maxRotatedFiles=5 also silently disagreed with
|
|
133
|
+
// the shipped value 7 — defaults living in two places always drift.)
|
|
134
|
+
export const Config = Schema.object({
|
|
135
|
+
root: Schema.string(),
|
|
136
|
+
filename: Schema.string().default('dsh-edge-agent.log'),
|
|
137
|
+
flushIntervalMs: Schema.natural().default(1000),
|
|
138
|
+
queueCapacity: Schema.natural().default(10_000),
|
|
139
|
+
maxFileSizeBytes: Schema.natural().default(50 * 1024 * 1024), // 50MB
|
|
140
|
+
maxFileAgeDays: Schema.number(), // §7.56: daily rotation by default
|
|
141
|
+
maxRotatedFiles: Schema.natural(),
|
|
142
|
+
level: Schema.union(['debug', 'info', 'warn', 'error']),
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* Singleton service exposed on `ctx.fileLogger`. Owns the in-memory queue
|
|
146
|
+
* and the background flush loop. The Cordis exporter callback installed
|
|
147
|
+
* by `installFileLogger` simply forwards to `export()` on this service.
|
|
148
|
+
*/
|
|
149
|
+
export class FileLoggerServiceImpl extends Service {
|
|
150
|
+
filePath;
|
|
151
|
+
flushIntervalMs;
|
|
152
|
+
queueCapacity;
|
|
153
|
+
maxFileSizeBytes;
|
|
154
|
+
maxFileAgeDays;
|
|
155
|
+
maxRotatedFiles;
|
|
156
|
+
/** Formatted lines waiting to be flushed to disk. */
|
|
157
|
+
queue = [];
|
|
158
|
+
/** Number of entries dropped because `queue` was full. */
|
|
159
|
+
droppedCount = 0;
|
|
160
|
+
/** Monotonically increasing serial so we can debug dropped-entry runs. */
|
|
161
|
+
flushSerial = 0;
|
|
162
|
+
flushTimer;
|
|
163
|
+
flushPromise = Promise.resolve();
|
|
164
|
+
constructor(ctx, config) {
|
|
165
|
+
super(ctx, 'fileLogger');
|
|
166
|
+
this.filePath = join(config.root, config.filename);
|
|
167
|
+
this.flushIntervalMs = config.flushIntervalMs;
|
|
168
|
+
this.queueCapacity = config.queueCapacity;
|
|
169
|
+
this.maxFileSizeBytes = config.maxFileSizeBytes;
|
|
170
|
+
this.maxFileAgeDays = config.maxFileAgeDays;
|
|
171
|
+
this.maxRotatedFiles = config.maxRotatedFiles;
|
|
172
|
+
}
|
|
173
|
+
/** Test helper returning the resolved log file path. */
|
|
174
|
+
filePathForTests() {
|
|
175
|
+
return this.filePath;
|
|
176
|
+
}
|
|
177
|
+
/** Test helper: how many entries have been dropped because the queue was full. */
|
|
178
|
+
droppedCountForTests() {
|
|
179
|
+
return this.droppedCount;
|
|
180
|
+
}
|
|
181
|
+
/** Start the periodic flush; idempotent. Call once from `installFileLogger`. */
|
|
182
|
+
start() {
|
|
183
|
+
if (this.flushTimer !== undefined)
|
|
184
|
+
return;
|
|
185
|
+
this.flushTimer = setInterval(() => {
|
|
186
|
+
void this.flush().catch(() => { });
|
|
187
|
+
}, this.flushIntervalMs);
|
|
188
|
+
// unref so the timer never holds the process open by itself.
|
|
189
|
+
if (typeof this.flushTimer.unref === 'function')
|
|
190
|
+
this.flushTimer.unref();
|
|
191
|
+
}
|
|
192
|
+
/** Stop the periodic flush; for tests and the dispose hook. */
|
|
193
|
+
stop() {
|
|
194
|
+
if (this.flushTimer !== undefined) {
|
|
195
|
+
clearInterval(this.flushTimer);
|
|
196
|
+
this.flushTimer = undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Cordis exporter callback. **Synchronous, no await**. Push the
|
|
201
|
+
* formatted line onto the queue; the background timer drains it.
|
|
202
|
+
*
|
|
203
|
+
* NEVER call `this.ctx.logger.*` from here — audit-log does that on
|
|
204
|
+
* its own errors (`audit-log.ts:266/309`), which would re-enter this
|
|
205
|
+
* method and spin the loop. We swallow everything silently and count
|
|
206
|
+
* drops; a one-shot stderr notice flags the first drop and every
|
|
207
|
+
* 1000th drop after.
|
|
208
|
+
*/
|
|
209
|
+
export(message) {
|
|
210
|
+
try {
|
|
211
|
+
const line = formatLine(message);
|
|
212
|
+
if (this.queue.length >= this.queueCapacity) {
|
|
213
|
+
this.queue.shift(); // drop oldest
|
|
214
|
+
this.droppedCount++;
|
|
215
|
+
if (this.droppedCount === 1 || this.droppedCount % 1000 === 0) {
|
|
216
|
+
// Direct stderr write — bypasses ctx.logger to avoid re-entry.
|
|
217
|
+
process.stderr.write(`[file-logger] dropped ${this.droppedCount} entries (queue full, capacity=${this.queueCapacity})\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.queue.push(line);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Intentionally empty — see method-level comment about loop avoidance.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Drain the queue to disk. Serialised through `flushPromise` so
|
|
228
|
+
* overlapping calls (timer + dispose) don't interleave writes.
|
|
229
|
+
*
|
|
230
|
+
* Uses `fs.open(path, 'a')` rather than audit-log's read-merge-write
|
|
231
|
+
* pattern — there is no upstream reader modifying the file, so
|
|
232
|
+
* append-at-the-tail is sufficient.
|
|
233
|
+
*/
|
|
234
|
+
async flush() {
|
|
235
|
+
if (this.queue.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
this.flushPromise = this.flushPromise.then(async () => {
|
|
238
|
+
// Snapshot the queue once; subsequent `export()` calls accumulate
|
|
239
|
+
// into the next batch. splice() returns the drained lines in order.
|
|
240
|
+
const drained = this.queue.splice(0, this.queue.length);
|
|
241
|
+
if (drained.length === 0)
|
|
242
|
+
return;
|
|
243
|
+
const serial = ++this.flushSerial;
|
|
244
|
+
try {
|
|
245
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
246
|
+
await this.checkAndRotate();
|
|
247
|
+
const payload = drained.join('');
|
|
248
|
+
const fd = await open(this.filePath, 'a');
|
|
249
|
+
try {
|
|
250
|
+
await fd.write(payload);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
await fd.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Swallow — never call ctx.logger. Best-effort persistence.
|
|
258
|
+
process.stderr.write(`[file-logger] flush #${serial} failed for ${drained.length} lines\n`);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
return this.flushPromise;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Size + age based rotation. Copied from audit-log's
|
|
265
|
+
* `checkAndRotate` (`audit-log.ts:278-311`) — the algorithm is the same
|
|
266
|
+
* but this exporter has no read-merge-write coupling, so we only rotate
|
|
267
|
+
* here (audit-log couples rotation with its write path).
|
|
268
|
+
*/
|
|
269
|
+
async checkAndRotate() {
|
|
270
|
+
if (!existsSync(this.filePath))
|
|
271
|
+
return;
|
|
272
|
+
try {
|
|
273
|
+
const stats = await stat(this.filePath);
|
|
274
|
+
// §7.56: calendar-day diff in LOCAL time (matches formatLocalIso's
|
|
275
|
+
// timestamp policy — "one day" is the operator's wall clock day, not
|
|
276
|
+
// a UTC day). The previous elapsed-time form (`now - mtime`) never
|
|
277
|
+
// fired for an active append-mode log because every write refreshes
|
|
278
|
+
// mtime to "moments ago".
|
|
279
|
+
const dayDiff = localDayIndex(new Date()) - localDayIndex(new Date(stats.mtimeMs));
|
|
280
|
+
const needsRotation = (this.maxFileSizeBytes > 0 && stats.size >= this.maxFileSizeBytes) ||
|
|
281
|
+
(this.maxFileAgeDays > 0 && dayDiff >= this.maxFileAgeDays);
|
|
282
|
+
if (!needsRotation)
|
|
283
|
+
return;
|
|
284
|
+
// Shift .N → .N+1, then current → .1. Highest index first so we
|
|
285
|
+
// never overwrite a file we still need to move.
|
|
286
|
+
for (let i = this.maxRotatedFiles; i >= 1; i--) {
|
|
287
|
+
const from = i === 1 ? this.filePath : `${this.filePath}.${i - 1}`;
|
|
288
|
+
const to = `${this.filePath}.${i}`;
|
|
289
|
+
if (existsSync(from)) {
|
|
290
|
+
await renameWithRetry(from, to);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Delete files beyond the keep window.
|
|
294
|
+
const oldest = `${this.filePath}.${this.maxRotatedFiles + 1}`;
|
|
295
|
+
if (existsSync(oldest)) {
|
|
296
|
+
const { rm } = await import('node:fs/promises');
|
|
297
|
+
await rm(oldest, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Rotation failure must not block the flush path — swallow.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Format a Cordis Message as one physical line:
|
|
307
|
+
* `<iso-ts> <LEVEL> [<plugin-name>] <message>\n`
|
|
308
|
+
*
|
|
309
|
+
* Mirrors the format the previous stdout exporter used
|
|
310
|
+
* (`src/dsh-adapter/index.ts:80-88` before Step 1.5), so operator grep
|
|
311
|
+
* habits (`grep '\[http-api\]' ~/.dsh-edge-agent/log/...`) carry over.
|
|
312
|
+
*
|
|
313
|
+
* Timestamp policy (§7.30): local-time ISO 8601 with numeric offset
|
|
314
|
+
* (e.g. `2026-09-03T09:41:30.824+08:00`), NOT `toISOString()` UTC `Z`.
|
|
315
|
+
* Operators in Asia/Shanghai tailing the ops log need wall-clock matches
|
|
316
|
+
* with their chat logs and `date` output. Epoch value is unchanged —
|
|
317
|
+
* any reader (jq, awk, datalog) can still parse the absolute moment.
|
|
318
|
+
* The audit log keeps epoch ms (machine-only schema; no human readers).
|
|
319
|
+
*
|
|
320
|
+
* Newlines inside an Error's `stack` are escaped to `⏎` so each log
|
|
321
|
+
* entry stays a single physical line; without this, a thrown Error
|
|
322
|
+
* spreads across 5-10 stack frames and `split('\n').filter(Boolean)`
|
|
323
|
+
* downstream counts them as separate entries, breaking rotation
|
|
324
|
+
* arithmetic and grep counts.
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* Calendar-day index in the system's LOCAL timezone: whole days since the
|
|
328
|
+
* epoch as seen from local wall-clock time. Only differences between two
|
|
329
|
+
* indexes are meaningful (`dayDiff >= maxFileAgeDays`). Using local
|
|
330
|
+
* components keeps day boundaries aligned with the log's own local ISO
|
|
331
|
+
* timestamps (§7.30 policy), so rotation happens at the operator's
|
|
332
|
+
* midnight, not UTC midnight.
|
|
333
|
+
*/
|
|
334
|
+
function localDayIndex(d) {
|
|
335
|
+
return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / 86_400_000);
|
|
336
|
+
}
|
|
337
|
+
function formatLocalIso(d) {
|
|
338
|
+
// Manual ISO 8601 in system TZ with offset, since `toISOString()` is
|
|
339
|
+
// hardcoded UTC and `Intl.DateTimeFormat` is locale-aware (varies by
|
|
340
|
+
// runtime). getXxx() methods on Date already use system local time.
|
|
341
|
+
// getTimezoneOffset returns minutes WEST of UTC; flip sign so positive
|
|
342
|
+
// offsets are east of UTC (ISO convention).
|
|
343
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
344
|
+
const padMs = (n) => String(n).padStart(3, '0');
|
|
345
|
+
const offsetMin = -d.getTimezoneOffset();
|
|
346
|
+
const sign = offsetMin >= 0 ? '+' : '-';
|
|
347
|
+
const absMin = Math.abs(offsetMin);
|
|
348
|
+
const offH = pad(Math.floor(absMin / 60));
|
|
349
|
+
const offM = pad(absMin % 60);
|
|
350
|
+
return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
351
|
+
`T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` +
|
|
352
|
+
`.${padMs(d.getMilliseconds())}${sign}${offH}:${offM}`);
|
|
353
|
+
}
|
|
354
|
+
function formatLine(message) {
|
|
355
|
+
const ts = formatLocalIso(new Date(message.ts));
|
|
356
|
+
const type = message.type.toUpperCase().padEnd(5);
|
|
357
|
+
const args = message.args ?? [];
|
|
358
|
+
const head = args[0] instanceof Error
|
|
359
|
+
? (args[0].stack || args[0].message)
|
|
360
|
+
: String(args[0] ?? '');
|
|
361
|
+
const tail = args.slice(1)
|
|
362
|
+
.map((a) => (a instanceof Error ? a.message : String(a)))
|
|
363
|
+
.join(' ');
|
|
364
|
+
const line = tail.length > 0 ? `${head} ${tail}` : head;
|
|
365
|
+
// Collapse internal newlines (mainly from Error.stack frames) so each
|
|
366
|
+
// entry is exactly one physical line. `⏎` is a visible marker that
|
|
367
|
+
// round-trips cleanly through `tail -F` and grep.
|
|
368
|
+
return `${ts} ${type} [${message.name}] ${line.replace(/\r?\n/g, ' ⏎ ')}\n`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Resolve the file-logger config from env vars with sensible defaults.
|
|
372
|
+
* Centralised here so tests can call it directly and `installFileLogger`
|
|
373
|
+
* stays a one-liner.
|
|
374
|
+
*
|
|
375
|
+
* Path resolution: `DSH_EDGE_LOG_ROOT` wins, then `USERPROFILE` (Windows),
|
|
376
|
+
* then `HOME` (POSIX). File name is fixed (`dsh-edge-agent.log`) so
|
|
377
|
+
* operators always know what file to tail.
|
|
378
|
+
*
|
|
379
|
+
* Level resolution: `DSH_EDGE_LOG_LEVEL` wins, otherwise `info`. The
|
|
380
|
+
* `info` default (2026-09-11, flipped from `debug` per user feedback
|
|
381
|
+
* "log 太冗余") suppresses LLM body debug calls in
|
|
382
|
+
* `cloud-llm-adapter.ts` / `local-llm-adapter.ts` — set
|
|
383
|
+
* `DSH_EDGE_LOG_LEVEL=debug` to re-enable.
|
|
384
|
+
*/
|
|
385
|
+
export function resolveLoggerConfig(env = process.env) {
|
|
386
|
+
// §7.102 layering (per field): explicit env param entry > config/edge.json
|
|
387
|
+
// `audit.*` (loadEdgeConfig also folds live DSH_EDGE_* env into the file
|
|
388
|
+
// view) > the literal below. The env-param layer keeps the unit tests'
|
|
389
|
+
// fake-env surface working; the file layer is what the desktop GUI finally
|
|
390
|
+
// gets when no env is exported.
|
|
391
|
+
const a = loadEdgeConfig(true).audit ?? {};
|
|
392
|
+
const homeBase = (env.USERPROFILE ?? env.HOME ?? '').toString();
|
|
393
|
+
const root = firstNonEmpty(env.DSH_EDGE_LOG_ROOT, a.logRoot)
|
|
394
|
+
?? join(homeBase, '.dsh-edge-agent', 'log');
|
|
395
|
+
// §7.56: daily rotation is the default (maxFileAgeDays=1). Override with
|
|
396
|
+
// DSH_EDGE_LOG_MAX_AGE_DAYS (0 disables age rotation, keeping size-only).
|
|
397
|
+
const ageDaysEnv = parseInt(env.DSH_EDGE_LOG_MAX_AGE_DAYS ?? '', 10);
|
|
398
|
+
const rotatedEnv = parseInt(env.DSH_EDGE_LOG_MAX_ROTATED ?? '', 10);
|
|
399
|
+
return {
|
|
400
|
+
root,
|
|
401
|
+
filename: 'dsh-edge-agent.log',
|
|
402
|
+
flushIntervalMs: 1000,
|
|
403
|
+
queueCapacity: 10_000,
|
|
404
|
+
maxFileSizeBytes: 50 * 1024 * 1024,
|
|
405
|
+
maxFileAgeDays: Number.isFinite(ageDaysEnv) && ageDaysEnv >= 0
|
|
406
|
+
? ageDaysEnv
|
|
407
|
+
: a.logMaxAgeDays ?? 1,
|
|
408
|
+
maxRotatedFiles: Number.isFinite(rotatedEnv) && rotatedEnv > 0
|
|
409
|
+
? rotatedEnv
|
|
410
|
+
: a.logMaxRotated ?? 7,
|
|
411
|
+
level: parseLogLevel(env.DSH_EDGE_LOG_LEVEL ?? a.logLevel, 'info'),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/** First non-empty string among the candidates, else undefined. */
|
|
415
|
+
function firstNonEmpty(...values) {
|
|
416
|
+
for (const v of values) {
|
|
417
|
+
if (typeof v === 'string' && v.length > 0)
|
|
418
|
+
return v;
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Install the file exporter on a fresh Cordis context. Called once at
|
|
424
|
+
* the very top of `apply()` (`src/dsh-adapter/index.ts`) before any
|
|
425
|
+
* other plugin can call `ctx.logger.*`.
|
|
426
|
+
*
|
|
427
|
+
* Gating:
|
|
428
|
+
* - `DSH_EDGE_LOG_FILE=false` disables (tests opt out).
|
|
429
|
+
* - `DSH_EDGE_LOG_FILE=force` 仍被接受(历史兼容),但已是默认行为。
|
|
430
|
+
*
|
|
431
|
+
* §7.118 (2026-10-26) — 守卫语义修正:
|
|
432
|
+
*
|
|
433
|
+
* 旧守卫是 `ctx.logger.exporters.size > 1 → return undefined`,理由是
|
|
434
|
+
* "宿主若已注册 file exporter,我们再写一份就是双写"。但这个 size 启发式
|
|
435
|
+
* **分不出控制台 exporter 和文件 exporter**:
|
|
436
|
+
*
|
|
437
|
+
* - 桌面 App 的 dsh-desktop-host 注册的是**控制台** exporter(输出走
|
|
438
|
+
* Electron 的 stdout,不落盘 —— `~/Library/Logs/DeepSeek Harness/`
|
|
439
|
+
* 实测是空目录)。size 变成 2 → 我们跳过 → **桌面 ops log 整份消失**,
|
|
440
|
+
* 最后一次写入停在 2026-10-25 19:47。
|
|
441
|
+
* - CLI profile 同理(这正是 `DSH_EDGE_LOG_FILE=force` 逃逸口存在的原因,
|
|
442
|
+
* 见本文件下方的历史注释)。
|
|
443
|
+
*
|
|
444
|
+
* 也就是说,这个守卫防的是一个**不会发生**的碰撞(两个 exporter 写同一个
|
|
445
|
+
* 文件),代价却是让 ops log 在两种真实宿主里都从不落盘。而"会不会写坏
|
|
446
|
+
* 文件"根本不成立:我们的输出路径是 **edge 独占**的
|
|
447
|
+
* `~/.dsh-edge-agent/log/`,宿主没有任何理由往那儿写。
|
|
448
|
+
*
|
|
449
|
+
* 真正需要防的"我们自己被装载两次"由下面的重复 `provide` try/catch 负责,
|
|
450
|
+
* 不该由 size 启发式兜。
|
|
451
|
+
*
|
|
452
|
+
* 现在:除非显式 `false`,一律注册。
|
|
453
|
+
*/
|
|
454
|
+
export function installFileLogger(ctx, resolved) {
|
|
455
|
+
if (process.env.DSH_EDGE_LOG_FILE === 'false')
|
|
456
|
+
return undefined;
|
|
457
|
+
// 历史上的 `force` 逃逸口:CLI profile 的宿主也注册了自己的 exporter,
|
|
458
|
+
// 旧守卫会让 ops log 静默不落盘。§7.118 起默认就注册,`force` 保留为
|
|
459
|
+
// 无操作别名,避免老脚本里的 `DSH_EDGE_LOG_FILE=force` 变成未知值。
|
|
460
|
+
const config = resolved ?? resolveLoggerConfig();
|
|
461
|
+
// Duplicate-load tolerance: in patch profiles the file-logger ROW applies
|
|
462
|
+
// first (owns the service name), and the bare aggregator row's apply then
|
|
463
|
+
// calls installFileLogger a SECOND time — `new Service` throws on the
|
|
464
|
+
// duplicate provide ("service fileLogger has been registered"), which
|
|
465
|
+
// used to kill the whole aggregator entry ("1 entry did not activate",
|
|
466
|
+
// observed on the headless dsh-edge-agent profile 2026-10-24). With the
|
|
467
|
+
// instance already provided, its exporter is already capturing — skip.
|
|
468
|
+
let service;
|
|
469
|
+
try {
|
|
470
|
+
service = new FileLoggerServiceImpl(ctx, config);
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
process.stderr.write(`[file-logger] skip duplicate install: ${err?.message ?? String(err)}\n`);
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
service.start();
|
|
477
|
+
// 2026-09-02 diagnostic: dump the web seam provider registry once boot
|
|
478
|
+
// settles. The "configured web provider deepseek-official is not
|
|
479
|
+
// registered" error we hit on `web_search` tool calls means
|
|
480
|
+
// web-search-deepseek's `apply()` never ran its
|
|
481
|
+
// `ctx.web.registerSearchProvider(...)` line — either because its
|
|
482
|
+
// `inject: ['web']` dep wasn't satisfied, or because
|
|
483
|
+
// `installSettingsSection` threw first. Knowing which one at boot saves
|
|
484
|
+
// a 90s chat round-trip.
|
|
485
|
+
//
|
|
486
|
+
// §7.169: `web` 是**延迟注入**(不再是本插件的硬依赖)—— 见本文件
|
|
487
|
+
// `export const inject` 上方的说明:硬依赖会把整个 ops log 通道拖到
|
|
488
|
+
// `web` 就绪之后,早 apply 的插件日志会静默丢失。延迟注入的回调在
|
|
489
|
+
// `web` 出现时触发;没有 `web` 的 profile 就不打这一行(与原行为等价:
|
|
490
|
+
// 原来是「读不到就打印 failed」)。
|
|
491
|
+
ctx.inject(['web'], (raw) => {
|
|
492
|
+
try {
|
|
493
|
+
const web = raw?.web;
|
|
494
|
+
const map = web?.searchProviders;
|
|
495
|
+
const size = map instanceof Map ? map.size : -1;
|
|
496
|
+
const ids = map instanceof Map ? [...map.keys()].join(',') : 'n/a';
|
|
497
|
+
ctx.logger?.info?.(`[web-diag] ctx.web registered=${web !== undefined} searchProviders.size=${size} ids=[${ids}]`);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
ctx.logger?.warn?.(`[web-diag] failed: ${err?.message ?? String(err)}`);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
// Bind the export callback to the service instance. Cordis calls this
|
|
504
|
+
// synchronously from inside its logger loop — never invoke ctx.logger
|
|
505
|
+
// from inside (would re-enter and loop).
|
|
506
|
+
//
|
|
507
|
+
// 2026-09-02: pass `levels: { default: <numeric> }` so the file-logger
|
|
508
|
+
// receives all messages at-or-above the configured threshold. Without
|
|
509
|
+
// this, Cordis's per-exporter level defaults to INFO and silently
|
|
510
|
+
// suppresses every `ctx.logger.debug(...)` call — including the LLM
|
|
511
|
+
// body debug calls in cloud-llm-adapter.ts / local-llm-adapter.ts.
|
|
512
|
+
// Cordis's own ring-buffer exporter keeps its hard-coded INFO level so
|
|
513
|
+
// the in-memory buffer stays tight while disk captures everything.
|
|
514
|
+
ctx.logger.exporter({
|
|
515
|
+
export: (msg) => service.export(msg),
|
|
516
|
+
levels: { default: LEVEL_NUMERIC[config.level] },
|
|
517
|
+
});
|
|
518
|
+
return service;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Stop the flush timer and drain the queue. Wired into the apply()
|
|
522
|
+
* dispose hook in `index.ts` so a clean shutdown loses at most one
|
|
523
|
+
* interval's worth of logs.
|
|
524
|
+
*/
|
|
525
|
+
export async function shutdownFileLogger(service) {
|
|
526
|
+
if (!service)
|
|
527
|
+
return;
|
|
528
|
+
service.stop();
|
|
529
|
+
await service.flush();
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Standard Cordis plugin apply — installs the file exporter on first
|
|
533
|
+
* load and returns a disposer that drains the queue on shutdown.
|
|
534
|
+
*
|
|
535
|
+
* Wired into repo-root `cordis.patch.yml` (per §7.25 + §7.31 — bundle is
|
|
536
|
+
* self-contained) as a standalone plugin id `file-logger` so this runs
|
|
537
|
+
* at boot, before any other dsh-edge-agent plugin (audit-log / http-api /
|
|
538
|
+
* etc.) emits log lines.
|
|
539
|
+
*
|
|
540
|
+
* Idempotency: `installFileLogger` already has a first-wins guard via
|
|
541
|
+
* `ctx.logger.exporters.size > 1`. Calling it twice is a no-op.
|
|
542
|
+
*/
|
|
543
|
+
export async function apply(ctx, config) {
|
|
544
|
+
// §7.102: the cordis row may omit root/level/rotation fields (the patches
|
|
545
|
+
// no longer pass env expressions). Layer: explicit row value >
|
|
546
|
+
// resolveLoggerConfig() (= config/edge.json audit.* > env > literal).
|
|
547
|
+
const base = resolveLoggerConfig();
|
|
548
|
+
const row = config;
|
|
549
|
+
const pick = (k) => (row[k] === undefined ? base[k] : row[k]);
|
|
550
|
+
const effective = {
|
|
551
|
+
root: pick('root'),
|
|
552
|
+
filename: pick('filename'),
|
|
553
|
+
flushIntervalMs: pick('flushIntervalMs'),
|
|
554
|
+
queueCapacity: pick('queueCapacity'),
|
|
555
|
+
maxFileSizeBytes: pick('maxFileSizeBytes'),
|
|
556
|
+
maxFileAgeDays: pick('maxFileAgeDays'),
|
|
557
|
+
maxRotatedFiles: pick('maxRotatedFiles'),
|
|
558
|
+
level: pick('level'),
|
|
559
|
+
};
|
|
560
|
+
const service = installFileLogger(ctx, effective);
|
|
561
|
+
return async () => {
|
|
562
|
+
await shutdownFileLogger(service);
|
|
563
|
+
};
|
|
564
|
+
}
|