ht32-code-mcp 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/DEPLOY.md +53 -39
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/PKG-INFO +1 -1
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/ht32-code-mcp-update.md +7 -5
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/pyproject.toml +1 -1
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/usage.py +46 -2
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/tests/test_tools.py +69 -1
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/uv.lock +1 -1
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/.gitignore +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/.gitkeep +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/README.md +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/e2e_scenario_a.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/e2e_scenario_b.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/smoke_stdio.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/__init__.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/__main__.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/exporter.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/index.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/knowledge.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/src/ht32_code_mcp/server.py +0 -0
- {ht32_code_mcp-0.2.0 → ht32_code_mcp-0.2.1}/tests/conftest.py +0 -0
|
@@ -174,46 +174,61 @@ docker exec 1Panel-openresty-ih7J nginx -t
|
|
|
174
174
|
|
|
175
175
|
4. 确认 `ls /opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles` 只有 `*.zip`(无残留临时目录)。
|
|
176
176
|
|
|
177
|
-
## 反馈与用量日志(P1-5,0.
|
|
177
|
+
## 反馈与用量日志(P1-5,0.2.0+)
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
> **与 ht32-wiki-mcp 统一**:两服务使用同一套实现(`usage.py`,两仓库逐字节
|
|
180
|
+
> 相同的副本)、同一套目录约定(同根 + 服务子目录)、同一套字段。
|
|
181
|
+
> 本服务写 `<FEEDBACK_ROOT>/code/`,wiki 写 `<FEEDBACK_ROOT>/wiki/`。
|
|
180
182
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
|
184
|
-
|
|
183
|
+
两条通道,按 UTC 日期分文件(JSON Lines):
|
|
184
|
+
|
|
185
|
+
| 通道 | 文件 | 触发 | 记录内容 |
|
|
186
|
+
|------|------|------|----------|
|
|
187
|
+
| 用量日志 | `usage_*.jsonl` | **服务端自动**(middleware 拦截所有工具调用) | 工具名、参数原文(剔除 `limit`/`max_length`/`out_dir`)、耗时、成败、错误、客户端 session |
|
|
188
|
+
| 反馈 | `feedback_*.jsonl` | **Agent 主动**(`submit_feedback` 工具) | 语义性问题:API 错 / 缺口 / 路径错 / 模板缺陷 |
|
|
185
189
|
|
|
186
|
-
自动通道保证调用事实完整(不依赖 Agent
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
自动通道保证调用事实完整(不依赖 Agent 配合),用于追踪用户需求与生成的模板;
|
|
191
|
+
反馈通道补 middleware 判断不了的语义问题。每条记录都带 `service` 字段
|
|
192
|
+
(`code`),便于两服务日志合并分析。
|
|
193
|
+
|
|
194
|
+
### 目录布局
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
/opt/ht32-mcp-feedback/ ← 宿主机单一入口,两个容器挂同一卷
|
|
198
|
+
├─ wiki/ ← ht32-wiki 容器写这里
|
|
199
|
+
└─ code/ ← ht32-code 容器写这里(本服务)
|
|
200
|
+
├─ usage_2026-09-21.jsonl
|
|
201
|
+
└─ feedback_2026-09-21.jsonl
|
|
202
|
+
```
|
|
189
203
|
|
|
190
204
|
### 环境变量
|
|
191
205
|
|
|
192
206
|
| 变量 | 默认 | 说明 |
|
|
193
207
|
|------|------|------|
|
|
194
|
-
| `
|
|
208
|
+
| `FEEDBACK_ROOT` | 空(= **关闭**,本地开发默认不落盘) | 日志根目录,服务名子目录自动拼接 |
|
|
209
|
+
| `CODE_LOG_DIR` | — | 已废弃,改用 `FEEDBACK_ROOT` |
|
|
195
210
|
|
|
196
|
-
> **隐私**:参数按原文记录,`query
|
|
211
|
+
> **隐私**:参数按原文记录,`query` / `question` 可能含用户项目信息。
|
|
197
212
|
> 该目录只应挂在受控服务器上,**不要提交进仓库**(定时脚本是
|
|
198
213
|
> `git reset --hard` 镜像模式,写进仓库也会被覆盖丢失)。
|
|
199
214
|
|
|
200
|
-
###
|
|
215
|
+
### 服务器侧落地
|
|
201
216
|
|
|
202
|
-
**第 1
|
|
217
|
+
**第 1 步:宿主机建目录**
|
|
203
218
|
|
|
204
219
|
```bash
|
|
205
|
-
mkdir -p /opt/ht32-
|
|
206
|
-
chmod 755 /opt/ht32-
|
|
220
|
+
mkdir -p /opt/ht32-mcp-feedback
|
|
221
|
+
chmod 755 /opt/ht32-mcp-feedback
|
|
207
222
|
```
|
|
208
223
|
|
|
209
|
-
**第 2
|
|
224
|
+
**第 2 步:两个 MCP 容器都挂同一卷**
|
|
210
225
|
|
|
211
|
-
|
|
|
212
|
-
|
|
213
|
-
|
|
|
214
|
-
|
|
|
226
|
+
| 服务 | 挂载 | 环境变量 |
|
|
227
|
+
|------|------|----------|
|
|
228
|
+
| ht32-wiki-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
|
|
229
|
+
| ht32-code-mcp | `/opt/ht32-mcp-feedback:/app/feedback` | `FEEDBACK_ROOT=/app/feedback` |
|
|
215
230
|
|
|
216
|
-
|
|
231
|
+
完整配置(code 侧,含现有项):
|
|
217
232
|
|
|
218
233
|
```
|
|
219
234
|
类型: uvx
|
|
@@ -226,38 +241,37 @@ chmod 755 /opt/ht32-code-logs
|
|
|
226
241
|
环境变量: CODE_ROOT=/app/code
|
|
227
242
|
BUNDLE_DIR=/app/bundles
|
|
228
243
|
BUNDLE_BASE_URL=https://mcp.s621.work/ht32-bundles
|
|
229
|
-
|
|
244
|
+
FEEDBACK_ROOT=/app/feedback
|
|
230
245
|
挂载: /opt/ht32-code:/app/code
|
|
231
246
|
/opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles:/app/bundles
|
|
232
|
-
/opt/ht32-
|
|
247
|
+
/opt/ht32-mcp-feedback:/app/feedback
|
|
233
248
|
```
|
|
234
249
|
|
|
235
|
-
###
|
|
250
|
+
### 验证
|
|
236
251
|
|
|
237
|
-
1. 调 `get_code_version`,应出现
|
|
252
|
+
1. 调 `get_code_version`,应出现
|
|
253
|
+
`反馈与用量日志:启用(/app/feedback/code)`。
|
|
238
254
|
仍显示「未配置」→ 环境变量没注入(查变量名 / 容器是否重建)。
|
|
239
255
|
|
|
240
256
|
2. 随便调几个工具,然后看日志:
|
|
241
257
|
|
|
242
258
|
```bash
|
|
243
|
-
tail -3 /opt/ht32-
|
|
259
|
+
tail -3 /opt/ht32-mcp-feedback/code/usage_$(date -u +%Y-%m-%d).jsonl
|
|
244
260
|
```
|
|
245
261
|
|
|
246
|
-
3.
|
|
262
|
+
3. 统计被请求的芯片型号(指导 P2-1 排优先级)——用 ht32-skills 的分析工具:
|
|
247
263
|
|
|
248
264
|
```bash
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
import sys, json, collections
|
|
252
|
-
c = collections.Counter()
|
|
253
|
-
for l in sys.stdin:
|
|
254
|
-
c.update([json.loads(l)['args'].get('chip', '?')])
|
|
255
|
-
for k, v in c.most_common():
|
|
256
|
-
print(f'{v:5d} {k}')
|
|
257
|
-
"
|
|
265
|
+
# 克隆 ht32-skills 后(或用本地副本)
|
|
266
|
+
python ht32-skills/scripts/analyze_feedback.py /opt/ht32-mcp-feedback
|
|
258
267
|
```
|
|
259
268
|
|
|
260
|
-
|
|
269
|
+
该工具(零依赖,仅标准库)合并 wiki + code 两服务日志,输出需求信号、
|
|
270
|
+
调用分布与失败、反馈类型、耗时 p50/p95、最近失败与反馈明细;支持
|
|
271
|
+
`--days N` / `--service` / `--json`。详见 ht32-skills 的 README。
|
|
272
|
+
|
|
273
|
+
4. 清理策略由定时同步脚本第 7 步负责(`LOG_KEEP_DAYS=30`,与同步日志同口径,
|
|
274
|
+
目录变量为 `FEEDBACK_ROOT`)。
|
|
261
275
|
|
|
262
276
|
## PyPI 包发布流程
|
|
263
277
|
|
|
@@ -291,8 +305,8 @@ uv lock && uv build && uv publish
|
|
|
291
305
|
| # | 任务 | 说明 | 状态 |
|
|
292
306
|
|---|------|------|------|
|
|
293
307
|
| 3.1 | MCP 工具实现(7 检索/导出 + 1 反馈) | `.work/mcp-server/src/ht32_code_mcp/`(FastMCP) | ✅ |
|
|
294
|
-
| 3.2 | 工具级测试 | `pytest`
|
|
295
|
-
| 3.3 | PyPI 发布(hatchling) | `ht32-code-mcp` 0.
|
|
308
|
+
| 3.2 | 工具级测试 | `pytest` 44 用例全绿 | ✅ |
|
|
309
|
+
| 3.3 | PyPI 发布(hatchling) | `ht32-code-mcp` 0.2.0(2026-09-21,含 bundle 通道 + 统一反馈/用量日志) | ✅ |
|
|
296
310
|
| 3.4 | 云端 git clone + 挂载 | `/opt/ht32-code:/app/code` | ✅ |
|
|
297
311
|
| 3.5 | 1Panel 部署(uvx + supergateway → streamableHttp stateful,镜像 next-uvx) | `https://mcp.s621.work/ht32-code` | ✅ |
|
|
298
312
|
| 3.6 | OpenResty 超时修复(`proxy_read_timeout 3600s`) | 修复 60s 空闲断连;2026-09-21 复核两端空闲 100s 后调用仍 200,未被 1Panel 回退 | ✅ |
|
|
@@ -52,9 +52,9 @@ INDEX_FILE="$CODE_DIR/.work/mcp-server/index/code_index.json"
|
|
|
52
52
|
BUNDLE_DIR="/opt/1panel/www/sites/mcp.s621.work/index/ht32-bundles"
|
|
53
53
|
BUNDLE_TTL_MIN=60
|
|
54
54
|
|
|
55
|
-
# 用量日志 / 反馈目录(1Panel 挂载到容器 /app/
|
|
55
|
+
# 用量日志 / 反馈目录(1Panel 挂载到容器 /app/feedback,与 wiki 同一卷)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
FEEDBACK_DIR="/opt/ht32-mcp-feedback"
|
|
58
58
|
|
|
59
59
|
# 日志目录(不存在则创建)
|
|
60
60
|
|
|
@@ -131,11 +131,13 @@ fi
|
|
|
131
131
|
|
|
132
132
|
# 7. 清理超期的用量日志 / 反馈(同样保留 LOG_KEEP_DAYS 天)
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
# 目录为同根 + 服务子目录(wiki/ 与 code/),故递归找两层下的 jsonl
|
|
135
|
+
|
|
136
|
+
if [ -d "$FEEDBACK_DIR" ]; then
|
|
137
|
+
OLD_LOGS=$(find "$FEEDBACK_DIR" -mindepth 2 -maxdepth 2 \( -name "usage_*.jsonl" -o -name "feedback_*.jsonl" \) -mtime +$LOG_KEEP_DAYS -print -delete 2>/dev/null | wc -l)
|
|
136
138
|
log "用量日志清理:删除 $OLD_LOGS 个过期文件"
|
|
137
139
|
else
|
|
138
|
-
log "用量日志目录不存在,跳过清理:$
|
|
140
|
+
log "用量日志目录不存在,跳过清理:$FEEDBACK_DIR"
|
|
139
141
|
fi
|
|
140
142
|
|
|
141
143
|
log "本次同步完成"
|
|
@@ -38,7 +38,7 @@ from typing import Any
|
|
|
38
38
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
39
39
|
|
|
40
40
|
# 实现版本号:两仓库副本必须一致(各仓测试会校验,防止只改一边造成漂移)
|
|
41
|
-
_IMPL_VERSION = "1.
|
|
41
|
+
_IMPL_VERSION = "1.1.0"
|
|
42
42
|
|
|
43
43
|
# 服务名 → 子目录名。新增服务在此登记。
|
|
44
44
|
SERVICE_WIKI = "wiki"
|
|
@@ -47,10 +47,49 @@ SERVICE_CODE = "code"
|
|
|
47
47
|
# 不记录的工具参数:分页/长度控制项无分析价值;out_dir 可能含用户本地路径
|
|
48
48
|
ARG_NOISE = frozenset({"limit", "max_length", "out_dir"})
|
|
49
49
|
|
|
50
|
+
# 工具内的"软失败"标记:两服务的工具用**字符串**返回错误而不抛异常,middleware
|
|
51
|
+
# 拿不到协议级 is_error,只能靠返回值文本判断。不识别会让真失败被记成 ok=true,
|
|
52
|
+
# 后果不只是统计失真——分析工具按 ok 过滤"需求信号",型号打错导致的
|
|
53
|
+
# "导出失败:不在模板参数表"会被当成一次真实的芯片需求,污染选型优先级结论。
|
|
54
|
+
#
|
|
55
|
+
# 判定口径 = **工具是否完成了它的工作**,故只收明确执行失败:
|
|
56
|
+
# - 错误: 参数校验/路径越界/IO 失败/反馈写盘失败(两服务)
|
|
57
|
+
# - 导出失败 code: export_project_bundle 无法产出工程
|
|
58
|
+
# - 请至少提供 wiki: compare_chips 参数校验
|
|
59
|
+
#
|
|
60
|
+
# 刻意**不算**失败的是"无数据"类回答(工具正常跑完,只是没结果):
|
|
61
|
+
# - 未找到匹配结果 / 未找到型号 'X' / 未找到外设 'X'
|
|
62
|
+
# - 不在支持列表 / 不在覆盖范围 / 暂无模板
|
|
63
|
+
# 它们记 ok=true 才能进入需求信号——"用户搜 BLDC 但知识库没有"正是
|
|
64
|
+
# 指导 P2-1 补内容的最强信号,标成失败会被分析工具当作噪声剔除。
|
|
65
|
+
SOFT_FAILURE_MARKERS = (
|
|
66
|
+
"错误:",
|
|
67
|
+
"导出失败",
|
|
68
|
+
"请至少提供",
|
|
69
|
+
)
|
|
70
|
+
# 只在返回值开头这段内匹配,避免正文里偶然出现标记词造成误判
|
|
71
|
+
_FAILURE_SCAN_CHARS = 80
|
|
72
|
+
|
|
50
73
|
USAGE_PREFIX = "usage"
|
|
51
74
|
FEEDBACK_PREFIX = "feedback"
|
|
52
75
|
|
|
53
76
|
|
|
77
|
+
def looks_like_failure(text: str) -> bool:
|
|
78
|
+
"""判断工具返回值是否为"软失败"(字符串形式的错误提示)。"""
|
|
79
|
+
head = (text or "").lstrip()[:_FAILURE_SCAN_CHARS]
|
|
80
|
+
return any(m in head for m in SOFT_FAILURE_MARKERS)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _result_text(result: Any) -> str:
|
|
84
|
+
"""取工具返回值中的文本(供软失败判断)。"""
|
|
85
|
+
parts: list[str] = []
|
|
86
|
+
for block in getattr(result, "content", None) or []:
|
|
87
|
+
text = getattr(block, "text", None)
|
|
88
|
+
if isinstance(text, str):
|
|
89
|
+
parts.append(text)
|
|
90
|
+
return "\n".join(parts)
|
|
91
|
+
|
|
92
|
+
|
|
54
93
|
def _now() -> str:
|
|
55
94
|
return datetime.now(timezone.utc).isoformat()
|
|
56
95
|
|
|
@@ -121,9 +160,14 @@ class UsageMiddleware(Middleware):
|
|
|
121
160
|
ok = True
|
|
122
161
|
try:
|
|
123
162
|
result = await call_next(context)
|
|
124
|
-
# 协议级错误标记;工具内以字符串返回的软失败不体现在此
|
|
125
163
|
if getattr(result, "is_error", False):
|
|
164
|
+
# 协议级错误
|
|
165
|
+
ok = False
|
|
166
|
+
error = _result_text(result)[:500]
|
|
167
|
+
elif looks_like_failure(_result_text(result)):
|
|
168
|
+
# 工具以字符串返回的软失败(如"导出失败:…")
|
|
126
169
|
ok = False
|
|
170
|
+
error = _result_text(result)[:500]
|
|
127
171
|
return result
|
|
128
172
|
except Exception as e:
|
|
129
173
|
error = f"{type(e).__name__}: {e}" # 异常上抛,但不能漏记
|
|
@@ -307,13 +307,45 @@ def test_version_reports_bundle_mode(monkeypatch):
|
|
|
307
307
|
def test_usage_impl_version_pinned():
|
|
308
308
|
"""两仓库的 usage.py 是逐字节相同副本,实现版本号用于防止只改一边。"""
|
|
309
309
|
from ht32_code_mcp import usage
|
|
310
|
-
assert usage._IMPL_VERSION == "1.
|
|
310
|
+
assert usage._IMPL_VERSION == "1.1.0"
|
|
311
311
|
assert usage.SERVICE_CODE == "code"
|
|
312
312
|
assert usage.SERVICE_WIKI == "wiki"
|
|
313
313
|
# 噪声参数含 out_dir(code 特有:可能含用户本地路径)
|
|
314
314
|
assert {"limit", "max_length", "out_dir"} <= usage.ARG_NOISE
|
|
315
315
|
|
|
316
316
|
|
|
317
|
+
def test_soft_failure_marker_semantics():
|
|
318
|
+
"""软失败判定口径 = 工具是否完成工作。
|
|
319
|
+
|
|
320
|
+
真失败(执行没成功)→ 算失败;"无数据"(正常跑完但没结果)→ 不算失败,
|
|
321
|
+
否则"用户搜 BLDC 但库里没有"这类最强缺口信号会被当噪声剔除。
|
|
322
|
+
"""
|
|
323
|
+
from ht32_code_mcp.usage import looks_like_failure as bad
|
|
324
|
+
|
|
325
|
+
# 真失败:参数/路径/IO/导出
|
|
326
|
+
assert bad("错误:path 不能为空。")
|
|
327
|
+
assert bad("错误:路径越界(只允许 raw/ 下文件):x")
|
|
328
|
+
assert bad("导出失败:芯片 'X' 不在模板参数表")
|
|
329
|
+
assert bad("导出失败(打包或写盘失败,检查 bundle 目录权限/空间):boom")
|
|
330
|
+
assert bad("错误:反馈未能写入。服务端可能未配置 FEEDBACK_ROOT,")
|
|
331
|
+
assert bad("请至少提供两个型号对比。") # wiki: compare_chips 参数校验
|
|
332
|
+
|
|
333
|
+
# 无数据 ≠ 失败:必须记 ok=true 才能进需求信号
|
|
334
|
+
assert not bad("未找到匹配知识页。建议:换模块官方名小写重试…")
|
|
335
|
+
assert not bad("未找到外设 'xxx' 的例程。可用外设目录(44 个):…")
|
|
336
|
+
assert not bad("芯片 'HT32F99999' 不在支持列表(当前 79 个型号,…")
|
|
337
|
+
assert not bad("系列 'F9' 不在覆盖范围。当前覆盖:…")
|
|
338
|
+
assert not bad("raw/templates/ 下暂无模板。")
|
|
339
|
+
assert not bad("未找到型号 'zzz'。可用型号:…")
|
|
340
|
+
|
|
341
|
+
# 正常成功返回值
|
|
342
|
+
assert not bad("找到 6 个 'gpio' 例程:…")
|
|
343
|
+
assert not bad("已导出 455 个文件到 …")
|
|
344
|
+
assert not bad("反馈已记录,感谢!问题类型:other")
|
|
345
|
+
# 正文后段偶然出现标记词不应误判(只扫开头 80 字符)
|
|
346
|
+
assert not bad("找到 3 条结果:\n" + "填充" * 60 + "\n注意:错误:的用法见页尾")
|
|
347
|
+
|
|
348
|
+
|
|
317
349
|
def test_resolve_log_dir_layout():
|
|
318
350
|
from ht32_code_mcp.usage import resolve_log_dir
|
|
319
351
|
# 同根 + 服务子目录
|
|
@@ -409,6 +441,42 @@ def test_usage_middleware_records_calls(tmp_path):
|
|
|
409
441
|
assert "deliberate" in fail_rec["error"]
|
|
410
442
|
|
|
411
443
|
|
|
444
|
+
def test_usage_middleware_records_soft_failure(tmp_path):
|
|
445
|
+
"""工具返回错误字符串(不抛异常)时,middleware 应记 ok=false 并留错误原文。"""
|
|
446
|
+
import asyncio
|
|
447
|
+
|
|
448
|
+
from fastmcp import Client, FastMCP
|
|
449
|
+
|
|
450
|
+
app = FastMCP("mw-soft")
|
|
451
|
+
app.add_middleware(UsageMiddleware(str(tmp_path / "logs"), "code",
|
|
452
|
+
mcp_version="0.2.1", kb_version="v0.1.0"))
|
|
453
|
+
|
|
454
|
+
@app.tool()
|
|
455
|
+
def soft_fail(chip: str) -> str:
|
|
456
|
+
return f"导出失败:芯片 '{chip}' 不在模板参数表"
|
|
457
|
+
|
|
458
|
+
@app.tool()
|
|
459
|
+
def no_data(peripheral: str) -> str:
|
|
460
|
+
return f"未找到外设 '{peripheral}' 的例程。可用外设目录(44 个):…"
|
|
461
|
+
|
|
462
|
+
async def run():
|
|
463
|
+
async with Client(app) as c:
|
|
464
|
+
await c.call_tool("soft_fail", {"chip": "HT32F99999"})
|
|
465
|
+
await c.call_tool("no_data", {"peripheral": "zzz"})
|
|
466
|
+
|
|
467
|
+
asyncio.run(run())
|
|
468
|
+
recs = [json.loads(l) for l in
|
|
469
|
+
next((tmp_path / "logs").glob("usage_*.jsonl"))
|
|
470
|
+
.read_text(encoding="utf-8").splitlines()]
|
|
471
|
+
fail_rec, nodata_rec = recs
|
|
472
|
+
# 真失败:ok=false 且留下错误原文
|
|
473
|
+
assert fail_rec["tool"] == "soft_fail" and fail_rec["ok"] is False
|
|
474
|
+
assert "不在模板参数表" in fail_rec["error"]
|
|
475
|
+
# 无数据:不算失败,保留在需求信号里
|
|
476
|
+
assert nodata_rec["tool"] == "no_data" and nodata_rec["ok"] is True
|
|
477
|
+
assert nodata_rec["error"] == ""
|
|
478
|
+
|
|
479
|
+
|
|
412
480
|
def test_usage_log_disabled_and_resilient(tmp_path):
|
|
413
481
|
"""log_dir 为空不落盘;目录不可写时调用仍成功(日志绝不中断服务)。"""
|
|
414
482
|
import asyncio
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|