dsh-my-observability 0.3.0 → 0.3.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/lib/client.src.js CHANGED
@@ -13,10 +13,18 @@
13
13
  * 样式走 DSH 语义 token(--dsw-alias-* / --dsw-font-*),随 activation
14
14
  * 注入、fiber teardown 卸载(HMR/禁用无残留)。
15
15
  *
16
+ * 侧边栏走**宿主原生扩展点**(issue #187 批 1),不再消费第三方侧边栏服务:
17
+ * sidebarRightTabs 注册页签类型(guide 胶囊供用户从右栏指南页打开),slots 的
18
+ * sidebar.right.pane.tab / .title 两个 keyed 席位注册面板与标题(key = 类型 id)。
19
+ * 原生能力经 Cordis 服务名 inject 获取,无需 require 任何
20
+ * @deepseek-ai/dsh-client-ui-* 包(官方范本:
21
+ * dsh-client-ui-sidebar-files/lib/client.js:681-711)。
22
+ *
16
23
  * BUILD NOTE: 本文件是模板源码,不是 DSH 实际服务的文件。scripts/build.mjs
17
- * 将片段文件(lib/parts/i18n.js / replay.js / git.js / styles.js,均为
18
- * 无 import/export 的纯函数声明文本;图标片段来自 dsh-shared 共享
19
- * client-parts,见 docs/UI规范.md)经下方 __PART_*__ 占位符(函数式
24
+ * 先编译 client TS 片段(src/client/parts/*.ts → lib/.client-build/parts/*.js),
25
+ * 再将这些片段(无 import/export 的纯函数声明文本;图标片段来自 dsh-shared
26
+ * 共享 client-parts,见 docs/UI规范.md;audit-view 片段来自 server 端产物
27
+ * lib/audit-view.js,剥离 export 前缀)经下方 __PART_*__ 占位符(函数式
20
28
  * replaceAll,避免 $&/$1 特殊解释)拼接进 factory 作用域,写出
21
29
  * lib/client.js —— 即 DSH 实际服务的产物。产物必须提交;CI 只对产物执行
22
30
  * node --check(见 scripts/test-all.sh / .github/workflows/ci.yml)。
@@ -29,7 +37,15 @@ window.__ModuleLoader__.load({
29
37
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
30
38
  const { createElement, useEffect, useState } = require('react')
31
39
 
40
+ // 原生页签身份(issue #187 批 1):id/kind = 迁移前 better-sidebar 的 tab id
41
+ // (每个面板一个类型,id 全局唯一且是席位的 key),order = 迁移前的指南页顺序。
42
+ const REPLAY_TAB_ID = 'dsh-my-observability:replay'
43
+ const GIT_TAB_ID = 'dsh-my-observability:git'
44
+ const REPLAY_TAB_ORDER = 40
45
+ const GIT_TAB_ORDER = 41
46
+
32
47
  // ── parts(scripts/build.mjs 拼接;顺序固定)───────────────────────
48
+ /*__PART_NATIVE_TABS__*/
33
49
  /*__PART_I18N__*/
34
50
  /*__PART_ICONS__*/
35
51
  /*__PART_AUDIT_VIEW__*/
@@ -39,35 +55,45 @@ window.__ModuleLoader__.load({
39
55
  /*__PART_GIT__*/
40
56
  /*__PART_STYLES__*/
41
57
 
42
- // ── 插件体:样式注入 + 两个页签注册 ────────────────────────────────
43
- exports.inject = ['betterSidebar']
58
+ // ── 插件体:样式注入 + 两个原生页签注册 ─────────────────────────────
59
+ exports.inject = ['slots', 'sidebarRightTabs']
60
+
61
+ /** 原生 tab body 席位:适配成面板契约(ReplayPanel 要 visible)。 */
62
+ function ReplayTabBody(props) {
63
+ const info = typeof props.useTabInfo === 'function' ? props.useTabInfo() : undefined
64
+ return createElement(ReplayPanel, { visible: info?.tab?.visible !== false })
65
+ }
66
+
67
+ /** Git 面板不接 props(迁移前组件工厂同样只是透传)。 */
68
+ function GitTabBody() {
69
+ return createElement(GitPanel, null)
70
+ }
44
71
 
45
72
  exports.apply = function apply(ctx) {
73
+ // 样式先注入:不依赖任何服务(服务缺失/时序未就绪也不影响面板配色)。
46
74
  ctx.effect(() => injectStyles(), 'dsh-my-observability: styles')
47
- const service = ctx.betterSidebar
48
- if (service === undefined) return
49
- ctx.effect(
50
- () =>
51
- service.registerTab({
52
- id: 'dsh-my-observability:replay',
53
- title: () => strings.replayTitle(),
54
- order: 40,
55
- single: true,
56
- component: (props) => createElement(ReplayPanel, props),
57
- }),
58
- 'dsh-my-observability: replay tab registration',
59
- )
60
- ctx.effect(
61
- () =>
62
- service.registerTab({
63
- id: 'dsh-my-observability:git',
64
- title: () => strings.gitTitle(),
65
- order: 41,
66
- single: true,
67
- component: (props) => createElement(GitPanel, props),
68
- }),
69
- 'dsh-my-observability: git tab registration',
70
- )
75
+
76
+ const tabs = ctx.sidebarRightTabs
77
+ const slots = ctx.slots
78
+ // 判空必须同时覆盖 null 与 undefined(typeof null 是 object,会骗过 === undefined)。
79
+ if (tabs == null || slots == null) return
80
+
81
+ registerNativeTab(ctx, tabs, slots, {
82
+ id: REPLAY_TAB_ID,
83
+ order: REPLAY_TAB_ORDER,
84
+ label: 'replay',
85
+ title: () => strings.replayTitle(),
86
+ Body: ReplayTabBody,
87
+ Title: nativeTabTitle(() => strings.replayTitle()),
88
+ })
89
+ registerNativeTab(ctx, tabs, slots, {
90
+ id: GIT_TAB_ID,
91
+ order: GIT_TAB_ORDER,
92
+ label: 'git',
93
+ title: () => strings.gitTitle(),
94
+ Body: GitTabBody,
95
+ Title: nativeTabTitle(() => strings.gitTitle()),
96
+ })
71
97
  }
72
98
 
73
99
  return module.exports
package/lib/constants.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dsh-my-observability — shared constants.
3
3
  */
4
- export const MAX_ARG_KEYS = 10
5
- export const MAX_TEXT_LEN = 200
6
- export const REVIEW_TIMEOUT_MS = 60000
7
- export const GIT_TIMEOUT_MS = 30000
4
+ export const MAX_ARG_KEYS = 10;
5
+ export const MAX_TEXT_LEN = 200;
6
+ export const REVIEW_TIMEOUT_MS = 60000;
7
+ export const GIT_TIMEOUT_MS = 30000;
package/lib/diff.js CHANGED
@@ -5,81 +5,86 @@
5
5
  * 行(含行号)与二进制标记,供规则审查引擎消费。全部为纯函数,可独立
6
6
  * 测试。
7
7
  */
8
-
9
8
  /** 解析 diff 文本:{ files, binary }。 */
10
9
  export function parseDiff(text) {
11
- if (typeof text !== 'string' || text === '') return { files: [], binary: false }
12
- const files = []
13
- let current = null
14
- let hunkLine = 0
15
- let binary = false
16
- for (const line of text.split('\n')) {
17
- const kind = classifyLine(line)
18
- if (kind === 'file') {
19
- current = { path: pathOf(line), insertions: 0, deletions: 0, addedLines: [], binary: false }
20
- files.push(current)
21
- hunkLine = 0
22
- continue
10
+ if (typeof text !== 'string' || text === '')
11
+ return { files: [], binary: false };
12
+ const files = [];
13
+ let current = null;
14
+ let hunkLine = 0;
15
+ let binary = false;
16
+ for (const line of text.split('\n')) {
17
+ const kind = classifyLine(line);
18
+ if (kind === 'file') {
19
+ current = { path: pathOf(line), insertions: 0, deletions: 0, addedLines: [], binary: false };
20
+ files.push(current);
21
+ hunkLine = 0;
22
+ continue;
23
+ }
24
+ if (current === null)
25
+ continue;
26
+ const applied = applyLine(current, kind, line, hunkLine);
27
+ hunkLine = applied.hunkLine;
28
+ if (applied.binary)
29
+ binary = true;
23
30
  }
24
- if (current === null) continue
25
- const applied = applyLine(current, kind, line, hunkLine)
26
- hunkLine = applied.hunkLine
27
- if (applied.binary) binary = true
28
- }
29
- return { files, binary }
31
+ return { files, binary };
30
32
  }
31
-
32
33
  /** 应用一行到当前文件(返回新的 hunkLine 与 binary 标记)。 */
33
34
  function applyLine(current, kind, line, hunkLine) {
34
- if (kind === 'binary') {
35
- current.binary = true
36
- return { hunkLine, binary: true }
37
- }
38
- if (kind === 'hunk') return { hunkLine: hunkLineOf(line) }
39
- if (kind === 'added') {
40
- current.insertions += 1
41
- current.addedLines.push({ line: hunkLine, text: line.slice(1) })
42
- return { hunkLine: hunkLine + 1 }
43
- }
44
- if (kind === 'deleted') {
45
- current.deletions += 1
46
- return { hunkLine }
47
- }
48
- if (kind === 'context') return { hunkLine: hunkLine + 1 }
49
- return { hunkLine }
35
+ if (kind === 'binary') {
36
+ current.binary = true;
37
+ return { hunkLine, binary: true };
38
+ }
39
+ if (kind === 'hunk')
40
+ return { hunkLine: hunkLineOf(line) };
41
+ if (kind === 'added') {
42
+ current.insertions += 1;
43
+ current.addedLines.push({ line: hunkLine, text: line.slice(1) });
44
+ return { hunkLine: hunkLine + 1 };
45
+ }
46
+ if (kind === 'deleted') {
47
+ current.deletions += 1;
48
+ return { hunkLine };
49
+ }
50
+ if (kind === 'context')
51
+ return { hunkLine: hunkLine + 1 };
52
+ return { hunkLine };
50
53
  }
51
-
52
54
  /** diff 行分类(file/binary/hunk/added/deleted/context/other)。 */
53
55
  function classifyLine(line) {
54
- if (line.startsWith('diff --git ')) return 'file'
55
- if (line.startsWith('Binary files ')) return 'binary'
56
- if (line.startsWith('@@ ')) return 'hunk'
57
- if (line.startsWith('+') && !line.startsWith('+++')) return 'added'
58
- if (line.startsWith('-') && !line.startsWith('---')) return 'deleted'
59
- if (line.startsWith(' ')) return 'context'
60
- return 'other'
56
+ if (line.startsWith('diff --git '))
57
+ return 'file';
58
+ if (line.startsWith('Binary files '))
59
+ return 'binary';
60
+ if (line.startsWith('@@ '))
61
+ return 'hunk';
62
+ if (line.startsWith('+') && !line.startsWith('+++'))
63
+ return 'added';
64
+ if (line.startsWith('-') && !line.startsWith('---'))
65
+ return 'deleted';
66
+ if (line.startsWith(' '))
67
+ return 'context';
68
+ return 'other';
61
69
  }
62
-
63
70
  /** 文件路径:`diff --git a/x b/y` → y(b/ 前缀剥除)。 */
64
71
  function pathOf(line) {
65
- const parts = line.split(' ')
66
- const b = parts[parts.length - 1] ?? ''
67
- return b.startsWith('b/') ? b.slice(2) : b
72
+ const parts = line.split(' ');
73
+ const b = parts[parts.length - 1] ?? '';
74
+ return b.startsWith('b/') ? b.slice(2) : b;
68
75
  }
69
-
70
76
  /** hunk 起始行号:`@@ -1,3 +10,4 @@` → 10(新文件侧)。 */
71
77
  function hunkLineOf(line) {
72
- const match = /@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line)
73
- return match !== null ? Number(match[1]) : 0
78
+ const match = /@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
79
+ return match !== null ? Number(match[1]) : 0;
74
80
  }
75
-
76
81
  /** 是否为测试文件(路径含 test/spec 标记)。 */
77
82
  export function isTestFile(path) {
78
- return /(^|\/)(test|tests|__tests__|spec)(\/|$)/.test(path) || /\.(test|spec)\./.test(path)
83
+ return /(^|\/)(test|tests|__tests__|spec)(\/|$)/.test(path) || /\.(test|spec)\./.test(path);
79
84
  }
80
-
81
85
  /** 是否为源码文件(排除纯文档/配置类;无扩展名视为源码)。 */
82
86
  export function isSourceFile(path) {
83
- if (isTestFile(path)) return false
84
- return !/\.(md|txt|json|yml|yaml|lock|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$/.test(path)
87
+ if (isTestFile(path))
88
+ return false;
89
+ return !/\.(md|txt|json|yml|yaml|lock|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$/.test(path);
85
90
  }
package/lib/git.js CHANGED
@@ -10,138 +10,137 @@
10
10
  * 提交流程:git add -A → git commit -m <完整消息>(单参数传入,消息由
11
11
  * 服务端生成,不拼接用户输入到 shell)。
12
12
  */
13
- import { execFile } from 'node:child_process'
14
- import { statSync } from 'node:fs'
15
- import { GIT_TIMEOUT_MS } from './constants.js'
16
-
13
+ import { execFile } from 'node:child_process';
14
+ import { statSync } from 'node:fs';
15
+ import { GIT_TIMEOUT_MS } from './constants.js';
17
16
  /** Conventional Commits 类型枚举。 */
18
- export const COMMIT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
19
-
17
+ export const COMMIT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'];
20
18
  /** 生成类型化提交消息(纯函数,可独立测试)。 */
21
- export function formatCommitMessage({ type, scope, description, body }) {
22
- const head = scope !== undefined && scope !== '' ? `${type}(${scope}): ${description}` : `${type}: ${description}`
23
- return body !== undefined && body !== '' ? `${head}\n\n${body}` : head
19
+ export function formatCommitMessage(input) {
20
+ const { type, scope, description, body } = input;
21
+ const head = scope !== undefined && scope !== '' ? `${type}(${scope}): ${description}` : `${type}: ${description}`;
22
+ return body !== undefined && body !== '' ? `${head}\n\n${body}` : head;
24
23
  }
25
-
26
24
  /** 校验并规整提交请求;非法返回 undefined(type/scope/description 规则见上)。 */
27
25
  export function parseCommitRequest(payload) {
28
- if (!isObject(payload)) return undefined
29
- if (!COMMIT_TYPES.includes(payload.type)) return undefined
30
- const description = stringOf(payload.description).trim()
31
- if (description === '') return undefined
32
- const scope = stringOf(payload.scope).trim()
33
- if (scope !== '' && !/^[a-z0-9-]+$/.test(scope)) return undefined
34
- if (payload.body !== undefined && typeof payload.body !== 'string') return undefined
35
- return { type: payload.type, scope, description, body: stringOf(payload.body).trim() }
26
+ if (!isObject(payload))
27
+ return undefined;
28
+ if (!COMMIT_TYPES.includes(payload.type))
29
+ return undefined;
30
+ const description = stringOf(payload.description).trim();
31
+ if (description === '')
32
+ return undefined;
33
+ const scope = stringOf(payload.scope).trim();
34
+ if (scope !== '' && !/^[a-z0-9-]+$/.test(scope))
35
+ return undefined;
36
+ if (payload.body !== undefined && typeof payload.body !== 'string')
37
+ return undefined;
38
+ return { type: payload.type, scope, description, body: stringOf(payload.body).trim() };
36
39
  }
37
-
38
40
  function isObject(value) {
39
- return value !== null && typeof value === 'object'
41
+ return value !== null && typeof value === 'object';
40
42
  }
41
-
42
43
  function stringOf(value) {
43
- return typeof value === 'string' ? value : ''
44
+ return typeof value === 'string' ? value : '';
44
45
  }
45
-
46
46
  /** 路径是否为存在的 git 仓库(目录存在且 git rev-parse 成功)。 */
47
47
  export async function isGitRepo(repoPath) {
48
- if (typeof repoPath !== 'string' || repoPath === '') return false
49
- try {
50
- if (!statSync(repoPath).isDirectory()) return false
51
- } catch {
52
- return false
53
- }
54
- const result = await runGit(repoPath, ['rev-parse', '--git-dir'])
55
- return result.ok
48
+ if (typeof repoPath !== 'string' || repoPath === '')
49
+ return false;
50
+ try {
51
+ if (!statSync(repoPath).isDirectory())
52
+ return false;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ const result = await runGit(repoPath, ['rev-parse', '--git-dir']);
58
+ return result.ok;
56
59
  }
57
-
58
60
  /** 执行 git 命令(不经 shell;超时 + 输出上限;失败返回 { ok:false, error })。 */
59
61
  function runGit(repoPath, args) {
60
- return new Promise((resolve) => {
61
- execFile(
62
- 'git',
63
- args,
64
- {
65
- cwd: repoPath,
66
- timeout: GIT_TIMEOUT_MS,
67
- maxBuffer: 16 * 1024 * 1024,
68
- },
69
- (error, stdout, stderr) => {
70
- if (error !== null) {
71
- const message = typeof stderr === 'string' && stderr.trim() !== '' ? stderr.trim() : error.message
72
- resolve({ ok: false, error: { message } })
73
- return
74
- }
75
- resolve({ ok: true, stdout, stderr })
76
- },
77
- )
78
- })
62
+ return new Promise((resolve) => {
63
+ execFile('git', args, {
64
+ cwd: repoPath,
65
+ timeout: GIT_TIMEOUT_MS,
66
+ maxBuffer: 16 * 1024 * 1024,
67
+ }, (error, stdout, stderr) => {
68
+ if (error !== null) {
69
+ const message = typeof stderr === 'string' && stderr.trim() !== '' ? stderr.trim() : error.message;
70
+ resolve({ ok: false, error: { message } });
71
+ return;
72
+ }
73
+ resolve({ ok: true, stdout, stderr });
74
+ });
75
+ });
79
76
  }
80
-
81
77
  /** 仓库状态:分支 + 变更清单(status --short --branch 解析)。 */
82
78
  export async function gitStatus(repoPath) {
83
- if (!(await isGitRepo(repoPath))) return { ok: false, error: { message: 'not a git repository' } }
84
- const result = await runGit(repoPath, ['status', '--short', '--branch'])
85
- if (!result.ok) return result
86
- const lines = result.stdout.split('\n').filter((line) => line !== '')
87
- const branch = parseBranch(lines[0] ?? '')
88
- const changes = lines.slice(1).map(parseChangeLine)
89
- return {
90
- ok: true,
91
- branch,
92
- changes,
93
- stagedCount: changes.filter((change) => change.staged).length,
94
- unstagedCount: changes.filter((change) => !change.staged).length,
95
- clean: changes.length === 0,
96
- }
79
+ if (!(await isGitRepo(repoPath)))
80
+ return { ok: false, error: { message: 'not a git repository' } };
81
+ const result = await runGit(repoPath, ['status', '--short', '--branch']);
82
+ if (!result.ok)
83
+ return result;
84
+ const lines = result.stdout.split('\n').filter((line) => line !== '');
85
+ const branch = parseBranch(lines[0] ?? '');
86
+ const changes = lines.slice(1).map(parseChangeLine);
87
+ return {
88
+ ok: true,
89
+ branch,
90
+ changes,
91
+ stagedCount: changes.filter((change) => change.staged).length,
92
+ unstagedCount: changes.filter((change) => !change.staged).length,
93
+ clean: changes.length === 0,
94
+ };
97
95
  }
98
-
99
96
  /** 分支行解析:`## main...origin/main [ahead 1]` → main。 */
100
97
  function parseBranch(line) {
101
- if (!line.startsWith('## ')) return ''
102
- const head = line.slice(3).split('...')[0].trim()
103
- return head
98
+ if (!line.startsWith('## '))
99
+ return '';
100
+ const head = line.slice(3).split('...')[0].trim();
101
+ return head;
104
102
  }
105
-
106
103
  /** 变更行解析:`XY path`(X=暂存区状态,Y=工作区状态;`??`=未跟踪)。 */
107
104
  function parseChangeLine(line) {
108
- const status = line.slice(0, 2)
109
- const path = line.slice(3)
110
- const staged = status[0] !== ' ' && status[0] !== '?'
111
- return { status, path, staged }
105
+ const status = line.slice(0, 2);
106
+ const path = line.slice(3);
107
+ const staged = status[0] !== ' ' && status[0] !== '?';
108
+ return { status, path, staged };
112
109
  }
113
-
114
110
  /** 差异文本:git diff(工作区)或 git diff --staged(暂存区)。 */
115
111
  export async function gitDiff(repoPath, staged) {
116
- if (!(await isGitRepo(repoPath))) return { ok: false, error: { message: 'not a git repository' } }
117
- const args = staged ? ['diff', '--staged'] : ['diff']
118
- const result = await runGit(repoPath, args)
119
- if (!result.ok) return result
120
- return { ok: true, text: result.stdout }
112
+ if (!(await isGitRepo(repoPath)))
113
+ return { ok: false, error: { message: 'not a git repository' } };
114
+ const args = staged ? ['diff', '--staged'] : ['diff'];
115
+ const result = await runGit(repoPath, args);
116
+ if (!result.ok)
117
+ return result;
118
+ return { ok: true, text: result.stdout };
121
119
  }
122
-
123
120
  /** 类型化提交:git add -A → git commit -m <消息>;返回 hash + 提交摘要。 */
124
121
  export async function gitCommit(repoPath, request) {
125
- if (!(await isGitRepo(repoPath))) return { ok: false, error: { message: 'not a git repository' } }
126
- const parsed = parseCommitRequest(request)
127
- if (parsed === undefined) return { ok: false, error: { message: 'invalid commit request' } }
128
- const message = formatCommitMessage(parsed)
129
- const addResult = await runGit(repoPath, ['add', '-A'])
130
- if (!addResult.ok) return addResult
131
- const commitResult = await runGit(repoPath, ['commit', '-m', message])
132
- if (!commitResult.ok) return commitResult
133
- const hash = parseCommitHash(commitResult.stdout)
134
- return { ok: true, hash, message, summary: commitSummary(commitResult.stdout) }
122
+ if (!(await isGitRepo(repoPath)))
123
+ return { ok: false, error: { message: 'not a git repository' } };
124
+ const parsed = parseCommitRequest(request);
125
+ if (parsed === undefined)
126
+ return { ok: false, error: { message: 'invalid commit request' } };
127
+ const message = formatCommitMessage(parsed);
128
+ const addResult = await runGit(repoPath, ['add', '-A']);
129
+ if (!addResult.ok)
130
+ return addResult;
131
+ const commitResult = await runGit(repoPath, ['commit', '-m', message]);
132
+ if (!commitResult.ok)
133
+ return commitResult;
134
+ const hash = parseCommitHash(commitResult.stdout);
135
+ return { ok: true, hash, message, summary: commitSummary(commitResult.stdout) };
135
136
  }
136
-
137
137
  /** 从 commit 输出提取 hash(`[main abc1234] ...`)。 */
138
138
  function parseCommitHash(stdout) {
139
- const match = /\[[^\]]+\s+([0-9a-f]{7,40})\]/.exec(stdout)
140
- return match !== null ? match[1] : ''
139
+ const match = /\[[^\]]+\s+([0-9a-f]{7,40})\]/.exec(stdout);
140
+ return match !== null ? match[1] : '';
141
141
  }
142
-
143
142
  /** 提交摘要:输出首行(`[main abc1234] message`)。 */
144
143
  function commitSummary(stdout) {
145
- const first = stdout.split('\n')[0].trim()
146
- return first.length > 120 ? `${first.slice(0, 120)}…` : first
144
+ const first = stdout.split('\n')[0].trim();
145
+ return first.length > 120 ? `${first.slice(0, 120)}…` : first;
147
146
  }
package/lib/index.js CHANGED
@@ -19,52 +19,51 @@
19
19
  * - ai.js — 可选 AI 审查增强(agents.create,失败降级)
20
20
  * - routes.js — /observability/api 路由
21
21
  */
22
- import { createStore } from './store.js'
23
- import { attachAuditListeners } from './audit.js'
24
- import { registerObservabilityRoutes } from './routes.js'
25
- import { createResourceMonitor } from './resource-monitor.js'
26
-
27
- export const name = 'dsh-my-observability'
28
-
29
- export const inject = ['webServer']
30
-
22
+ import { createStore } from './store.js';
23
+ import { attachAuditListeners } from './audit.js';
24
+ import { registerObservabilityRoutes } from './routes.js';
25
+ import { createResourceMonitor } from './resource-monitor.js';
26
+ export const name = 'dsh-my-observability';
27
+ export const inject = ['webServer'];
28
+ /** 从配置构建 ObservabilityOptions。 */
29
+ function buildOptions(config) {
30
+ return {
31
+ aiProvider: config?.aiProvider,
32
+ aiModel: config?.aiModel,
33
+ aiCwd: config?.aiCwd,
34
+ aiReview: config?.aiReview !== false,
35
+ aiTimeoutMs: Number.isFinite(config?.aiTimeoutMs) && config?.aiTimeoutMs > 0
36
+ ? config?.aiTimeoutMs
37
+ : 60000,
38
+ };
39
+ }
31
40
  export function apply(ctx, config) {
32
- // ── 配置(应用层 config 覆盖,默认全部开启)─────────────────────────
33
- const options = {
34
- aiReview: config?.aiReview !== false,
35
- aiTimeoutMs: Number.isFinite(config?.aiTimeoutMs) && config.aiTimeoutMs > 0 ? config.aiTimeoutMs : 60000,
36
- }
37
-
38
- // ── 审计存储:会话隔离 + 持久化 + 重启恢复 ──────────────────────────
39
- const store = createStore(ctx)
40
-
41
- // ── 事件监听(只读观察;waterfall 一律透传 next())──────────────────
42
- attachAuditListeners(ctx, store.record)
43
-
44
- // ── 资源监控 + 降级看门狗(15s 采样 CPU/内存/审计写入速率;写放大/文件
45
- // 超限连续触发时自动降级停落盘,资源回归后自动恢复——issue #127)────
46
- const monitor = createResourceMonitor(ctx, {
47
- intervalMs: config?.resourceIntervalMs,
48
- limits: config?.resourceLimits,
49
- onDegrade: () => {
50
- store.setPersistEnabled(false)
51
- ctx.logger.warn(
52
- '[dsh-my-observability] 资源看门狗:审计写入速率/文件大小连续超限,已暂停落盘(事件仍在内存,恢复后全量快照补齐)',
53
- )
54
- },
55
- onRecover: () => {
56
- store.setPersistEnabled(true)
57
- ctx.logger.warn('[dsh-my-observability] 资源看门狗:写入速率回归正常,已恢复审计落盘')
58
- },
59
- })
60
- monitor.start()
61
-
62
- // ── 路由(查询 / git 工具 / diff 审查 / 资源)───────────────────────
63
- registerObservabilityRoutes(ctx, store, monitor, options)
64
-
65
- // ── 卸载冲刷:清防抖定时器 + 立即落盘 + 停采样 ──────────────────────
66
- ctx.effect(() => {
67
- monitor.stop()
68
- return store.dispose
69
- }, 'dsh-my-observability: persistence teardown')
41
+ // ── 配置(应用层 config 覆盖,默认全部开启)─────────────────────────
42
+ const options = buildOptions(config);
43
+ // ── 审计存储:会话隔离 + 持久化 + 重启恢复 ──────────────────────────
44
+ const store = createStore(ctx);
45
+ // ── 事件监听(只读观察;waterfall 一律透传 next())──────────────────
46
+ attachAuditListeners(ctx, store.record);
47
+ // ── 资源监控 + 降级看门狗(15s 采样 CPU/内存/审计写入速率;写放大/文件
48
+ // 超限连续触发时自动降级停落盘,资源回归后自动恢复——issue #127)────
49
+ const monitor = createResourceMonitor(ctx, {
50
+ intervalMs: config?.resourceIntervalMs,
51
+ limits: config?.resourceLimits,
52
+ onDegrade: () => {
53
+ store.setPersistEnabled(false);
54
+ ctx.logger.warn('[dsh-my-observability] 资源看门狗:审计写入速率/文件大小连续超限,已暂停落盘(事件仍在内存,恢复后全量快照补齐)');
55
+ },
56
+ onRecover: () => {
57
+ store.setPersistEnabled(true);
58
+ ctx.logger.warn('[dsh-my-observability] 资源看门狗:写入速率回归正常,已恢复审计落盘');
59
+ },
60
+ });
61
+ monitor.start();
62
+ // ── 路由(查询 / git 工具 / diff 审查 / 资源)───────────────────────
63
+ registerObservabilityRoutes(ctx, store, monitor, options);
64
+ // ── 卸载冲刷:清防抖定时器 + 立即落盘 + 停采样 ──────────────────────
65
+ ctx.effect(() => {
66
+ monitor.stop();
67
+ return store.dispose;
68
+ }, 'dsh-my-observability: persistence teardown');
70
69
  }