dsh-logalyze 0.0.0-stage → 0.0.0

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.
Files changed (39) hide show
  1. package/README.md +281 -2
  2. package/cordis.patch.yml +3 -0
  3. package/examples/order-service/.dsh/logalyze.yaml +7 -0
  4. package/examples/order-service/.dsh/skills/log-analysis/SKILL.md +30 -0
  5. package/examples/order-service/README.md +23 -0
  6. package/examples/order-service/sample-logs/order-service.log +8 -0
  7. package/examples/order-service/src/main/java/com/example/order/OrderController.java +15 -0
  8. package/examples/order-service/src/main/java/com/example/order/OrderService.java +24 -0
  9. package/lib/client.js +1687 -0
  10. package/lib/client.js.map +1 -0
  11. package/lib/index.js +3051 -0
  12. package/lib/index.js.map +1 -0
  13. package/package.json +99 -4
  14. package/src/abort.ts +26 -0
  15. package/src/analyze/contract.ts +122 -0
  16. package/src/analyze/server.ts +194 -0
  17. package/src/analyze/topics.ts +105 -0
  18. package/src/client/api.ts +133 -0
  19. package/src/client/icon.tsx +26 -0
  20. package/src/client/index.ts +59 -0
  21. package/src/client/page.tsx +600 -0
  22. package/src/client/styles.ts +88 -0
  23. package/src/config.ts +244 -0
  24. package/src/dispatch/download.ts +153 -0
  25. package/src/dispatch/prompt.ts +89 -0
  26. package/src/dispatch/tool.ts +422 -0
  27. package/src/host.ts +250 -0
  28. package/src/index.ts +83 -0
  29. package/src/repos/lookup.ts +77 -0
  30. package/src/repos/manifest.ts +131 -0
  31. package/src/repos/registry.ts +308 -0
  32. package/src/service.ts +437 -0
  33. package/src/settings/contract.ts +232 -0
  34. package/src/settings/overlay.ts +92 -0
  35. package/src/settings/routes.ts +193 -0
  36. package/src/skill/entry.ts +199 -0
  37. package/src/skill-update/change.ts +81 -0
  38. package/src/skill-update/proposal.ts +116 -0
  39. package/src/skill-update/tool.ts +339 -0
package/README.md CHANGED
@@ -1,3 +1,282 @@
1
- # Temporary Holding Version
1
+ # 观象(dsh-logalyze)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **观其象,知其因。**
4
+
5
+ 观象是面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)的日志分析插件。
6
+
7
+ 名称取意于《周易》的观象思想:以日志为象,以推理为径,从复杂的系统运行痕迹中洞察异常、关联线索、追溯根因。
8
+
9
+ 界面上显示的插件名是「观象」;npm 包名、入口命令、工具、路由、配置文件和代码标识统一使用 logalyze。
10
+
11
+ ## 项目状态
12
+
13
+ 初版(未发布到 npm):打通「`/logalyze` 入口 → 识别应用 → 派发子 Agent 到应用代码仓库 → 汇总结论」这条链路,附带一个示例应用仓库。接口仍可能调整。
14
+
15
+ ## 核心能力
16
+
17
+ 观象的能力按名称拆分为三个层次,由浅入深:
18
+
19
+ | 能力 | 关注点 | 规划内容 |
20
+ |---|---|---|
21
+ | **观** | 看见 | 解析日志、提取线索、发现异常 |
22
+ | **象** | 理解 | 理解系统行为、关联事件、构建问题全貌 |
23
+ | **推演** | 求因 | 借助 AI Agent 自主分析、验证假设、定位根因 |
24
+
25
+ ### 观:解析日志,发现异常
26
+
27
+ - 解析常见的结构化与半结构化日志格式,统一为可检索的事件记录。
28
+ - 提取时间、级别、模块、请求标识、错误信息等关键字段作为线索。
29
+ - 识别错误突增、异常堆栈、频率偏离等可疑信号。
30
+
31
+ ### 象:关联事件,构建全貌
32
+
33
+ - 按时间线、请求链路和组件关系关联分散的日志事件。
34
+ - 将多个异常信号归并为同一问题,还原问题发生的上下文。
35
+ - 呈现系统在问题时段内的行为全貌,为后续推演提供依据。
36
+
37
+ ### 推演:自主分析,定位根因
38
+
39
+ - 由 AI Agent 基于「观」与「象」的结果提出根因假设。
40
+ - 主动检索更多日志与上下文,对假设逐一验证或排除。
41
+ - 输出可追溯的分析过程与结论,说明每个判断所依据的日志证据。
42
+
43
+ ## 工作方式
44
+
45
+ 观象是分体结构:插件只负责识别与派发,各应用的日志分析知识跟着应用代码放在应用自己的仓库里。
46
+
47
+ ```
48
+ 用户:/logalyze 订单服务昨晚 21 点下单报 500,日志见附件
49
+ │
50
+ ▼
51
+ 入口 SKILL(意图识别)── 先 fetch 各仓库,应用目录由远端最新提交的 .dsh/logalyze.yaml 生成
52
+ │ logalyze_dispatch({ tasks: [{ app, question, logs }] })
53
+ ▼
54
+ 派发工具:校验应用 id → 下载 URL 日志 → 并行启动子 Agent,等待全部结束
55
+ │ 子 Agent 工作目录 = 应用仓库,首条消息以 /<skill> 开头加载该应用的日志分析 SKILL
56
+ ▼
57
+ 父 Agent 按应用分节汇总,涉及多个应用时给出跨应用的推演
58
+ ```
59
+
60
+ - **一个入口,两种触发。** 入口 SKILL 是 `/logalyze`,dsh 在 Web、TUI、ACP 以及经 dsh_channels 的飞书消息中都能识别它。有可用应用且 `autoInvoke` 开启(默认)时,模型也能自行加载:dsh 给顶层 Agent 的 skill 目录里,观象的描述会列出每个可用应用的名称、id、别名和日志特征(总长不超过 500 字,放不下的只计数),用户说「分析这份订单服务的日志」或贴出带 `OrderController` 的报错时,模型会先加载观象。日志明显不属于任何已注册应用时,入口 SKILL 让模型按普通方式处理,不派发也不追问。子 Agent 的工作目录在 dsh-repo 的镜像目录内,那里不列出观象。应用增删时 dsh 会追加一条新的 skill 目录消息;想要确定触发时仍用 `/logalyze`。
61
+ - **派发由代码完成。** 模型只给出应用 id、问题和日志位置;仓库目录、应用 SKILL、并发上限都由插件决定。应用 id 必须与注册表一一对应,否则整次派发被拒绝。
62
+ - **日志一律以绝对路径交给子 Agent。** 附件和本机文件直接传路径;消息里的对象存储下载链接由派发工具下载(只允许白名单域名,不带鉴权,限制大小与耗时)。日志包不解压,由应用的日志分析 SKILL 在运行目录里解压。
63
+ - **应用目录随时最新。** 每次加载 `/logalyze` 都先经 dsh-repo `git fetch` 各仓库(间隔由 `fetchIntervalMs` 控制,默认每次),再从远端分支最新提交的 git 对象读取应用描述生成应用目录。fetch 不改动任何检出,所以正在进行的分析不会挡住更新。最多等待 `refreshWaitMs`,超时就先用已有的目录,fetch 在后台继续。
64
+ - **代码快照不可变。** 子 Agent 在 dsh-repo 按提交创建的 `git worktree` 只读快照里工作。快照创建后不再改动,同一提交的派发共用一份;没有分析在用、且已不是最新提交的快照会被删除。
65
+ - **运行目录。** 每次派发在父会话目录下创建 `.logalyze/runs/<runId>/<序号>-<应用 id>/`,下载的日志放在这里,子 Agent 也只在这里写文件;应用仓库对子 Agent 只读。
66
+ - **等待式派发。** 工具等待所有子 Agent 结束后一次返回,一条消息对应一张完整回复;超过派发超时或调用被取消时停止仍在运行的子 Agent。子会话中不提供派发工具。
67
+ - **SKILL 改进(可选,默认关闭)。** 子 Agent 发现应用的日志分析 SKILL 有欠缺时,可以在运行目录写出修改提议;经用户授权后,观象经 [dsh-repo](https://github.com/modestoma/dsh-repo) 把它提交到应用仓库的新分支并创建合并请求(GitLab MR 或 GitHub PR),见 [SKILL 改进](#skill-改进)。
68
+
69
+ ## 安装与配置
70
+
71
+ 观象以 dsh bundle 交付(`package.json` 的 `dsh.bundle.patch` 指向 `cordis.patch.yml`),Host 半侧运行 `lib/index.js`,浏览器半侧运行 `lib/client.js`。
72
+
73
+ 安装已发布的 npm 版本(推荐):
74
+
75
+ ```bash
76
+ # 在 deepseek-harness 仓库内:把 npm 包安装进 web profile
77
+ DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add dsh-logalyze
78
+ ```
79
+
80
+ 本地 checkout 开发时:
81
+
82
+ ```bash
83
+ pnpm build
84
+ DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add link:/path/to/dsh-logalyze
85
+ ```
86
+
87
+ npm 发行包已包含构建产物;本地 `link:` 安装前必须先执行 `pnpm build`。安装或修改后重启 dsh。包内的 `cordis.patch.yml` 把 `dsh-logalyze` 加入 profile 的 bundle 列表,并提供稳定 `id: logalyze` 的 Loader 行。可在 profile 的 patch 中按该 id 配置默认值:
88
+
89
+ ```yaml
90
+ - id: logalyze
91
+ config:
92
+ repos:
93
+ - url: git@github.com:example/order-service.git
94
+ - url: https://github.com/example/pay-service.git
95
+ branch: release # 可选,默认跟随远端默认分支
96
+ fetchIntervalMs: 0 # 加载 /logalyze 时距上次拉取超过该时间的仓库先 fetch;0 = 每次
97
+ maxAppsPerRequest: 4 # 单次派发的任务上限
98
+ dispatchTimeoutMs: 1800000 # 等待全部子 Agent 的上限
99
+ download:
100
+ allowedHosts: [oss.example.com] # 同时允许子域名;为空时不下载 URL
101
+ maxBytes: 536870912
102
+ timeoutMs: 600000
103
+ subagentProvider: spawn # 子 Agent 使用的 ctx.subagents provider
104
+ refreshWaitMs: 15000 # 加载 /logalyze 时等待 fetch 的上限;0 = 不等待
105
+ autoInvoke: true # 任务匹配已注册应用时模型可自行加载入口 SKILL;false = 只能输入 /logalyze
106
+ skillUpdates: # SKILL 改进,见下文
107
+ enabled: false # 默认关闭
108
+ maxFiles: 16 # 一次提议最多新增或修改的文件数
109
+ maxFileBytes: 262144 # 提议中单个文件的大小上限
110
+ skillUpdateAuthor: # 可选:SKILL 改进提交的作者;省略时是 dsh-repo 中该主机令牌的主人
111
+ name: 观象
112
+ email: logalyze@example.com
113
+ analyze: # 日志平台分析接口,见下文
114
+ enabled: false # 默认关闭
115
+ analyzeTokenRef: LOGALYZE_ANALYZE_TOKEN # 接口访问令牌在 DSH 凭据库中的名称
116
+ ```
117
+
118
+ 配置在加载时校验,不合法时插件加载失败。git 地址支持 https/http/ssh/git/file URL、`user@host:path` 和绝对路径。
119
+
120
+ ### 仓库拉取与访问令牌
121
+
122
+ 应用仓库由 [dsh-repo](https://github.com/modestoma/dsh-repo) 拉取,观象自己不运行 git,需要同时安装 dsh-repo;没有 dsh-repo 或其服务版本不兼容时仓库拉取失败,原因显示在设置面板。
123
+
124
+ - **镜像与快照在 dsh-repo。** 仓库镜像在 dsh-repo 的 `mirror.dir`(默认 `$DSH_HOME/dsh-repo/mirrors`)下,只用来 fetch,不检出文件;git 超时用 dsh-repo 的 `mirror.gitTimeoutMs`。
125
+ - **访问令牌是 dsh-repo 的主机。** https 地址属于 dsh-repo 中某个主机时,拉取自动使用该主机的令牌(GitLab 用户名 `oauth2`)。主机可以写在 dsh-repo 的 `hosts` 中,也可以在观象面板「访问令牌」分页添加;令牌存入 DSH 凭据库,面板只显示是否已设置。ssh 地址、`user@host:path`、其他主机和本地路径使用本机已有的 git 凭据(SSH key 或 credential helper),不会弹出交互式输入。
126
+
127
+ 凭据库文件只防其他系统用户。与 DSH 中的模型 API Key 一样,Agent 的命令以同一系统用户运行,文件权限不能把令牌与 Agent 隔离(见 `@deepseek-ai/dsh-credentials-local` 文档);建议使用权限最小、设置过期时间的令牌。
128
+
129
+ 子 Agent 的命令在 dsh 的文件沙箱中运行:仓库只读,只有父会话的项目目录可写,运行目录就在其中。Linux 的 bubblewrap 沙箱会把 `/tmp` 换成空的临时目录,所以 dsh-repo 的 `mirror.dir` 不要放在 `/tmp` 下,否则子 Agent 的命令看不到仓库。
130
+
131
+ ### 设置面板
132
+
133
+ DSH Web 左侧栏的「观象」入口打开整页设置面板:
134
+
135
+ | 分页 | 内容 |
136
+ |---|---|
137
+ | 应用仓库 | git 地址列表(分支可选)、每个仓库的拉取状态、提交号、应用描述与不可用原因,单个或全部重新拉取 |
138
+ | 访问令牌 | dsh-repo 的主机列表:选 GitLab 或 GitHub,填地址、别名和令牌即可添加,立即生效。每行显示令牌是否已设置及来源,可替换或清除;面板添加的主机可以删除,dsh-repo cordis.yml 中的主机只能改令牌 |
139
+ | 日志下载 | 允许下载的域名、单文件大小上限、下载超时 |
140
+ | 分析接口 | [日志平台分析接口](#日志平台分析接口)的开关、访问令牌(只显示是否已设置)、飞书渠道是否就绪,以及调用地址与字段摘要 |
141
+ | 高级 | 更新间隔、单次应用数上限、派发超时、SKILL 改进开关与提议上限,恢复 cordis.yml 配置 |
142
+
143
+ 面板保存的值写入 `$DSH_HOME/logalyze/settings.json`(只保存与 cordis.yml 不同的字段),按与配置相同的规则校验;主机保存在 dsh-repo,令牌值存入 DSH 凭据库。面板接口 `/logalyze/*` 使用 DSH 自己的登录与来源校验,写操作另做同源校验。
144
+
145
+ ## 应用仓库约定
146
+
147
+ 每个应用仓库需要两样东西(完整示例见 [examples/order-service](examples/order-service)):
148
+
149
+ `.dsh/logalyze.yaml` 应用描述:
150
+
151
+ ```yaml
152
+ id: order-service # 小写短横线格式,派发与分析请求按它对应应用
153
+ name: 订单服务 # 显示名,也是子 Agent 的标签
154
+ description: 下单、库存扣减与支付回调
155
+ aliases: [订单, 下单, order] # 用户对它的其他叫法
156
+ logHints: [order-service, OrderController] # 能识别本应用日志的字符串
157
+ skill: log-analysis # 对应 .dsh/skills/log-analysis/SKILL.md
158
+ ```
159
+
160
+ `.dsh/skills/<skill>/SKILL.md`:这个应用的日志分析流程(如何解压日志包、日志格式、代码结构、常见问题)。它不能设置 `user-invocable: false`,因为派发时用 `/<skill>` 加载。
161
+
162
+ 插件拉取仓库后检查这两个文件;缺失、不合法,或多个仓库声明同一个 id 时,相关应用不可用,原因显示在设置面板。应用描述和 SKILL 必须是普通文件,不能是符号链接。删除地址不会删除 dsh-repo 的本地镜像。
163
+
164
+ ## SKILL 改进
165
+
166
+ 开启 `skillUpdates.enabled`(设置面板「高级」→「SKILL 改进」)后,分析中发现的 SKILL 欠缺可以经用户授权后提交为合并请求。提交和创建合并请求经 [dsh-repo](https://github.com/modestoma/dsh-repo) 的 `dshRepos` 服务完成:应用仓库所在的主机要在 dsh-repo 中配置令牌(dsh-repo 的 `hosts` 或面板「访问令牌」);没有该服务或版本不符时工具报错。
167
+
168
+ 1. **子 Agent 写提议。** 子 Agent 的任务消息多出一节说明:把要改的文件从 `.dsh/skills/<skill>/` 复制到运行目录的 `skill-proposal/files/`(保持相对路径)后修改,理由写进 `skill-proposal/reason.md`。仓库对子 Agent 仍然只读。
169
+ 2. **派发结果标出提议。** 派发结束后,观象检查已完成任务的提议:只接受普通文件和受限的文件名、UTF-8 文本、文件数与大小上限,`SKILL.md` 不能设置 `user-invocable: false`。合格的提议在结果中给出改动行数,并在运行目录写出 `skill-proposal.diff`;不合格的给出原因。
170
+ 3. **父 Agent 展示并提交。** 入口 SKILL 让父 Agent 在汇总回复后附上改动和 diff,再调用 `logalyze_propose_skill`(参数为任务的运行目录和一句话摘要)。只接受本次 DSH 运行中由同一会话派发过的任务;应用、仓库和分析时的提交取自派发记录。
171
+ 4. **请求授权。** 工具通过 DSH 的审批服务(`ctx.approval`,`@deepseek-ai/dsh-user-approval`)请求一次性授权,说明将提交的仓库、分支、目标分支、改动文件和理由。经 dsh_channels 使用时显示为飞书决策卡片,在 DSH Web 中显示为审批提示;观象与 dsh_channels 之间没有代码依赖。只有「允许」才提交;拒绝、撤回或没有可回答的界面时都不提交。会话的审批策略为 `never`(例如 DSH 的「完全访问」权限模式)时请求一律被拒绝,所以要在「询问」类权限模式下使用;DSH 组合没有审批服务时工具报错。
172
+ 5. **提交新分支并创建合并请求。** 观象进程(不是 Agent)经 `dshRepos` 以分析时的提交为起点建新分支 `logalyze/skill-<应用 id>-<改动摘要前缀>`,提交 `.dsh/skills/<skill>/` 下有改动的文件,再创建合入跟踪分支的合并请求,返回它的链接。同一改动的分支已存在时不再请求授权,也不提交;等待授权期间分支被别人建出时同样不提交。从不提交到已有分支或默认分支,合并由仓库的评审流程决定;合并后下次 `/logalyze` 拉取即生效。提交成功但合并请求创建失败时,工具返回分支和失败原因,可以在代码托管平台上手动创建。
173
+
174
+ 注意:
175
+
176
+ - 支持 GitLab(包括自建实例)和 GitHub。应用仓库的 git 地址(https、ssh 或 `user@host:path`)要能被 dsh-repo 识别为已配置主机上的仓库;dsh-repo 的令牌在 GitLab 上需要 `api` 权限,在 GitHub 上需要 Contents 和 Pull requests 的读写权限。
177
+ - 有写权限的令牌可以改动它能访问的所有项目。建议用只覆盖应用仓库的项目或组访问令牌,设置过期时间,用 dsh-repo 的 `repos` 限定范围,并保护应用仓库的默认分支。
178
+ - 合并请求以 dsh-repo 令牌主人的身份创建,提交作者是 `skillUpdateAuthor`,省略时也是令牌主人。飞书群里点「允许」的人可能不是令牌主人;合并请求描述写明了来源会话与分析提交。
179
+ - 日志内容不可信,可能诱导模型写出不当的 SKILL。观象只限制改动范围,内容需要授权的人和合并请求的评审者把关。
180
+
181
+ ## 日志平台分析接口
182
+
183
+ 日志平台调用观象的分析接口:观象经 [dsh_channels](https://github.com/modestoma/dsh_channels) 的 `feishuTopics` 服务在指定群里发一张分析卡(发起人、触发时间、应用、错误码和日志详情页表格),在卡片下开话题,把平台给的 prompt 原样交给一个新 Session(prompt 开头的斜杠命令由 DSH 展开成对应 SKILL,例如 `/logalyze`),分析过程在话题内流式输出。分析结束后分析卡的状态标签随之更新,群里通过准入的成员都可以在该话题内追问。日志包地址写在 prompt 里,下载、解压、超时都由 prompt 调用的 SKILL 处理(Session 的沙箱需要能访问日志存储地址)。
184
+
185
+ **前提**:
186
+
187
+ - dsh_channels 已加载并提供 `feishuTopics` 服务(版本 1)。没有这个服务时观象不注册接口;服务版本不兼容时观象记一条警告,也不注册。设置面板「分析接口」分页显示飞书渠道是否就绪。
188
+ - 只有带 Web 的 profile(web、desktop)有这个接口,headless 没有。
189
+
190
+ **开启**:在设置面板「分析接口」打开开关(`analyze.enabled`,默认关,保存后立即生效),并在同一页保存访问令牌(建议 32 位以上随机串)。令牌存入 DSH 凭据库的 `LOGALYZE_ANALYZE_TOKEN`(名称可用 `analyzeTokenRef` 修改),也可以设置同名环境变量或写进 `.env`。接口挂在 DSH Web 服务上(与 Web 界面同一端口),路径 `/open-api/logalyze` 放在 DSH 的 `/api` 之外,网关可以把它与需要登录的 DSH 接口分开放行;DSH 通过反向代理挂在前缀下时,地址随之带上前缀(如 `https://<域名>/dsh/open-api/logalyze`)。该路由不走 DSH 登录 cookie,访问令牌是唯一凭据。开关关闭时所有请求返回 404;令牌每次请求都重新读取,轮换后下一次请求生效;令牌未设置时所有请求返回 503。
191
+
192
+ **请求**:
193
+
194
+ ```http
195
+ POST /open-api/logalyze
196
+ Authorization: Bearer <令牌>
197
+ Idempotency-Key: <平台生成的唯一值,1-200 个可见 ASCII 字符>
198
+ Content-Type: application/json
199
+
200
+ {
201
+ "requesterUserId": "a1b2c3d4",
202
+ "triggeredAt": "2026-10-22 19:30",
203
+ "app": "camera",
204
+ "errorCode": "E1001",
205
+ "logDetails": [
206
+ { "name": "设备 A 崩溃日志", "url": "https://logs.example.com/detail/1" },
207
+ { "name": "设备 B 崩溃日志", "url": "https://logs.example.com/detail/2" }
208
+ ],
209
+ "chatId": "oc_xxx",
210
+ "prompt": "/logalyze https://logs.example.com/1.zip https://logs.example.com/2.zip 请分析这些日志"
211
+ }
212
+ ```
213
+
214
+ - `requesterUserId`:平台发起人的飞书 user_id,分析卡 @ 此人;路由记录里也以此人作为发起人。
215
+ - `triggeredAt`:平台上发起分析的时间,1–64 字符,原样显示在分析卡上(时区和格式由平台决定)。
216
+ - `app` / `errorCode`:只允许 `A-Z a-z 0-9 . _ -`(1–128 字符),决定工作区 `<dsh_channels baseDir>/analyze/<app>/<errorCode>/`;同一应用同一错误码的所有分析都在这个工作区里,每次分析各有一个话题目录。
217
+ - `logDetails`:触发分析的日志在日志平台上的详情页,1–50 条,每条 `name`(1–200 字符)+ `url`(http/https)。只在分析卡的表格里列出,观象不会访问这些地址。
218
+ - `chatId`:目标群,必须是 dsh_channels 已授权的群(`/auth group add` 或其面板「访问与授权」),机器人需在群内;dsh_channels `accessMode: open` 时不校验。
219
+ - `prompt`:1–100000 字符,原样作为首条用户消息,不追加任何内容。
220
+ - 只接受以上 7 个字段,多余字段返回 400。
221
+
222
+ **响应**:校验通过、分析卡发出后立即返回 `202`:
223
+
224
+ ```json
225
+ { "ok": true, "chatId": "oc_xxx", "messageId": "om_xxx" }
226
+ ```
227
+
228
+ `messageId` 是分析卡的消息 id,分析话题挂在它下面。同步错误都带中文 `error`:`401` 令牌无效、`404` 接口开关未打开(或接口未注册)、`405` 不是 POST、`400` 参数或 `Idempotency-Key` 不合法、`403` 目标群未授权、`409` 同一个 `Idempotency-Key` 已被内容不同的请求使用、`413` 请求体超过 1 MiB、`415` 不是 JSON、`502` 分析卡发送失败(如机器人不在群里)、`503` 飞书未连接或未设置令牌。
229
+
230
+ **去重**:同一个 `Idempotency-Key` 再次请求(包括并发重试)原样返回第一次的结果,不会再发卡、再分析。记录由 dsh_channels 永久保存。分析卡发出后的失败(话题创建、Session 创建)不会回滚这条记录:分析卡的状态变成「失败」并写明原因,需要重试时请换一个新的 `Idempotency-Key`。
231
+
232
+ **群里看到的**:先是分析卡「@发起人 于 <triggeredAt> 发起了对 X 应用 Y 错误码不良日志的分析」,下面是日志详情页表格,标题栏标签为「分析中」;紧接着话题内一张流式卡片,首轮分析在这里流式输出,带停止按钮。首轮结束后分析卡的标签变成「已完成」「已停止」或「失败」;DSH 在首轮进行中重启时,重新连上飞书后仍是「分析中」的分析卡会改成「已中断」,在话题内回复即可继续。分析话题是群内共享的:群里任何通过 dsh_channels 准入的成员都可以在话题内追问、停止或回答审批卡。卡片、话题与权限的细节见 dsh_channels README 的「群话题服务(feishuTopics)」。
233
+
234
+ ## 通过飞书使用
235
+
236
+ 在飞书里使用 `/logalyze` 不需要观象与 [dsh_channels](https://github.com/modestoma/dsh_channels) 之间的任何接口:dsh_channels 把不在自己命令表中的 `/` 开头消息作为普通 prompt 交给 dsh,dsh 识别 `/logalyze` 后展开入口 SKILL。飞书附件的宿主路径会出现在消息里,入口 SKILL 会把它作为日志路径派发。只有[日志平台分析接口](#日志平台分析接口)经 dsh_channels 的 `feishuTopics` 服务发卡开话题;观象只在开发时依赖 dsh_channels 的类型,运行时按服务名查找,没有加载 dsh_channels 时其余功能不受影响。
237
+
238
+ ## 开发
239
+
240
+ ```sh
241
+ pnpm install
242
+ pnpm check # 类型检查(Host 与 Client)+ 单元测试 + 构建 + 两个加载自检
243
+ ```
244
+
245
+ `@modesto_ma/dsh-channels` 是开发依赖,只用来引入 `feishuTopics` 服务的类型(`@modesto_ma/dsh-channels/topics`,`import type`),构建产物不包含它。
246
+
247
+ ## 发布
248
+
249
+ 版本号由 semantic-release 按 Conventional Commits 计算。推送到 `dev` 后,检查通过就自动发布 beta;正式版**只在手动触发时发布**,推送到 `main` 只跑检查。只有 `feat`、`fix` 或破坏性变更会产生新版本,其余类型的提交不发布。PR 标题需符合 `<type>(<scope>): <summary>`,type 取 `feat | fix | docs | test | refactor | chore | ci`。
250
+
251
+ | 分支 | 触发 | 版本 | npm dist-tag |
252
+ | --- | --- | --- | --- |
253
+ | `main` | 手动 | 正式版,如 `0.1.0` | `latest` |
254
+ | `dev` | 推送自动 | 预发布版,如 `0.1.0-beta.1` | `beta` |
255
+
256
+ 发布正式版:在 GitHub Actions 的 `pipeline` workflow 点击 **Run workflow** 并选择 `main`。`dry_run` 默认勾选,只预览下一个版本号;取消勾选才真正发布 npm 包、打 tag 并创建 GitHub Release。在 `dev` 上手动触发同样可用,用来预览或补发 beta。安装 beta 版用 `dsh-logalyze@beta`。
257
+
258
+ | 路径 | 内容 |
259
+ |---|---|
260
+ | `src/index.ts` | Cordis 插件入口 |
261
+ | `src/service.ts` | 生效设置、注册表、入口 SKILL 与派发工具的注册 |
262
+ | `src/repos/` | dsh-repo 服务查找、应用描述校验、应用注册表 |
263
+ | `src/skill/entry.ts` | `/logalyze` 入口 SKILL |
264
+ | `src/dispatch/` | 派发工具、日志下载、子 Agent 首条消息 |
265
+ | `src/skill-update/` | SKILL 修改提议的校验、diff、授权与提交 |
266
+ | `src/analyze/` | 日志平台分析接口:请求校验、令牌、到 `feishuTopics` 的转换 |
267
+ | `src/settings/` | 面板契约、覆盖文件、HTTP 路由 |
268
+ | `src/client/` | 左侧栏入口与设置页面 |
269
+ | `examples/order-service/` | 示例应用仓库 |
270
+
271
+ ## 路线图
272
+
273
+ - [x] 确定插件结构与 bundle 打包方式(分体派发初版)
274
+ - [ ] 按日志中的版本号切换应用代码版本
275
+ - [ ] 观:通用日志解析与异常检测工具,供各应用 SKILL 复用
276
+ - [ ] 象:事件关联与问题全貌
277
+ - [ ] 推演:Agent 驱动的根因分析
278
+ - [ ] 使用文档与示例
279
+
280
+ ## 许可证
281
+
282
+ 待定。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: logalyze
3
+ name: 'dsh-logalyze'
@@ -0,0 +1,7 @@
1
+ # 观象应用描述:观象拉取本仓库后读取此文件,把应用加入 /logalyze 的应用目录。
2
+ id: order-service
3
+ name: 订单服务
4
+ description: 下单、库存扣减与支付回调
5
+ aliases: [订单, 下单, order]
6
+ logHints: [order-service, OrderController, OrderService]
7
+ skill: log-analysis # 对应 .dsh/skills/log-analysis/SKILL.md
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: log-analysis
3
+ description: 订单服务的日志分析流程:解压日志包、定位异常、对照本仓库代码给出根因。
4
+ ---
5
+
6
+ # 订单服务日志分析
7
+
8
+ 你在订单服务的代码仓库中工作,任务消息给出了问题、日志路径和一个可写的运行目录。仓库只读。
9
+
10
+ ## 1. 准备日志
11
+
12
+ - `.tar.gz` / `.tgz`:`tar -xzf <日志包> -C <运行目录>`
13
+ - `.zip`:`unzip -o <日志包> -d <运行目录>`
14
+ - `.gz`(单文件):`gzip -dc <文件> > <运行目录>/<文件名去掉 .gz>`
15
+ - 纯文本日志直接读取,不要复制。
16
+
17
+ ## 2. 定位异常
18
+
19
+ 1. 先按任务给出的时间范围和关键标识(订单号、用户 id、traceId)过滤。
20
+ 2. 查找 `ERROR`、`Exception`、`Caused by`,记录第一条异常和它之前同一 traceId 的请求。
21
+ 3. 本服务的日志格式:`时间 级别 [线程] [traceId] 类名 - 消息`。
22
+
23
+ ## 3. 对照代码
24
+
25
+ - 异常堆栈里的 `com.example.order.*` 类位于 `src/main/java/com/example/order/`,按文件名和行号阅读对应代码。
26
+ - 库存扣减由 `InventoryClient` 调用库存服务,`503` 或超时属于下游问题,不是本服务缺陷。
27
+
28
+ ## 4. 输出
29
+
30
+ 按任务消息要求的五部分返回结论、关键证据、时间线、假设与待验证、建议。证据中的代码位置写成 `文件:行号`。
@@ -0,0 +1,23 @@
1
+ # 示例应用仓库:订单服务
2
+
3
+ 演示观象要求的应用仓库结构:
4
+
5
+ ```
6
+ .dsh/logalyze.yaml 应用描述(id、名称、别名、日志特征、分析 SKILL 名)
7
+ .dsh/skills/log-analysis/SKILL.md 日志分析 SKILL,观象派发时用 /log-analysis 加载
8
+ src/ 应用代码,子 Agent 对照它定位根因
9
+ sample-logs/order-service.log 一份示例日志(促销商品没有 SKU 导致 NullPointerException)
10
+ ```
11
+
12
+ 本地试用:把本目录复制出来初始化为 git 仓库,再在「观象」面板中添加它的绝对路径作为 git 地址。
13
+
14
+ ```sh
15
+ cp -r examples/order-service /tmp/order-service && cd /tmp/order-service
16
+ git init -q && git add -A && git commit -qm init
17
+ ```
18
+
19
+ 然后在对话中输入:
20
+
21
+ ```
22
+ /logalyze 订单服务昨晚 21 点下单报 500,日志在 /tmp/order-service/sample-logs/order-service.log
23
+ ```
@@ -0,0 +1,8 @@
1
+ 2026-10-24 21:03:11.204 INFO [http-nio-8080-exec-7] [7f3a9c21] c.e.order.OrderController - POST /orders userId=u_10086 items=2
2
+ 2026-10-24 21:03:11.209 INFO [http-nio-8080-exec-7] [7f3a9c21] c.e.order.OrderService - draft order created orderId=O20261024210311
3
+ 2026-10-24 21:03:11.214 INFO [http-nio-8080-exec-7] [7f3a9c21] c.e.order.InventoryClient - deduct sku=SKU-1001 qty=1 ok
4
+ 2026-10-24 21:03:11.215 ERROR [http-nio-8080-exec-7] [7f3a9c21] c.e.order.OrderController - create order failed
5
+ java.lang.NullPointerException: Cannot invoke "String.trim()" because the return value of "com.example.order.OrderItem.getSku()" is null
6
+ at com.example.order.OrderService.create(OrderService.java:18)
7
+ at com.example.order.OrderController.createOrder(OrderController.java:13)
8
+ 2026-10-24 21:03:11.216 INFO [http-nio-8080-exec-7] [7f3a9c21] c.e.order.OrderController - response status=500 elapsed=12ms
@@ -0,0 +1,15 @@
1
+ package com.example.order;
2
+
3
+ /** HTTP entry for order creation. */
4
+ public class OrderController {
5
+ private final OrderService service;
6
+
7
+ public OrderController(OrderService service) {
8
+ this.service = service;
9
+ }
10
+
11
+ /** POST /orders */
12
+ public Order createOrder(CreateOrderRequest request) {
13
+ return service.create(request.getUserId(), request.getItems());
14
+ }
15
+ }
@@ -0,0 +1,24 @@
1
+ package com.example.order;
2
+
3
+ import java.util.List;
4
+
5
+ /** Creates orders and reserves their stock. */
6
+ public class OrderService {
7
+ private final InventoryClient inventory;
8
+
9
+ public OrderService(InventoryClient inventory) {
10
+ this.inventory = inventory;
11
+ }
12
+
13
+ /** Create an order for the given items; every item must name a SKU. */
14
+ public Order create(String userId, List<OrderItem> items) {
15
+ Order order = Order.draft(userId);
16
+ for (OrderItem item : items) {
17
+ // Items added from a promotion carry their SKU in promotionSku.
18
+ String sku = item.getSku().trim();
19
+ inventory.deduct(sku, item.getQuantity());
20
+ order.add(item);
21
+ }
22
+ return order.confirm();
23
+ }
24
+ }