@bushizhanghan/dsh-publish-gate 0.9.0 → 0.11.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.
- package/CHANGELOG.md +13 -0
- package/README.md +5 -1
- package/lib/engine.js +82 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
- `tool-no-description` (error), `tool-description-thin` (warn), `tool-no-parameters` (error): the host
|
|
6
|
+
renders name + description + parameters into the prompt and nothing else, so those three fields are the
|
|
7
|
+
tool's entire specification. A tool with no description is a name in a list; a tool that reads args with no
|
|
8
|
+
schema never receives them. All nine of my plugins were clean, so this guards the shape rather than a
|
|
9
|
+
specific crash — the first two fixtures that had `description: 'd'` did fail, which is the point.
|
|
10
|
+
## 0.10.0
|
|
11
|
+
|
|
12
|
+
- `client-double-load` (warn): the client probe now evaluates the bundle a second time against the same
|
|
13
|
+
`window`, with a loader that throws on a repeated registration. A bundle inserted by two patches (its own,
|
|
14
|
+
plus any sidebar plugin that injects `dsh-client-modules`) aborted web boot this way. A window-level guard
|
|
15
|
+
makes it a no-op, which is what the recommended bundle shape now shows.
|
|
3
16
|
## 0.9.0
|
|
4
17
|
|
|
5
18
|
- `checkChangelog()`: `changelog-missing-version` (warn), `changelog-no-versions` (warn), `no-changelog` (info).
|
package/README.md
CHANGED
|
@@ -104,6 +104,9 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
104
104
|
| `tool-present-call-throws` | `presentCall` 抛——调用记录渲染时炸 |
|
|
105
105
|
| `tool-bad-card-kind` | `card: 'generic'` 的 `kind` 不在宿主认识的那一组里(`other` / `execute` / `search` / `read` / `edit` / `delete` / `fetch`)。注册不报错,卡片渲染坏掉——我五个插件一起踩过 |
|
|
106
106
|
| `tool-bad-parameter-type` | `parameters` 里某个 `type` 写错(`"strin"`)。宿主把 `parameters` 渲染进 prompt、但**从不校验**调用值,所以这个 typo 只是悄悄把类型提示弄丢 |
|
|
107
|
+
| `tool-no-description` | 工具没有 description。模型是靠 name + description + parameters 挑工具的,没有描述就只剩名字——工具发出去了,但基本选不中 |
|
|
108
|
+
| `tool-description-thin` | description 少于 30 字符,或者就是把工具名重复一遍。等于没写 |
|
|
109
|
+
| `tool-no-parameters` | execute 读 `args.x`,但整个 `parameters` 都没有。模型拿不到参数清单,不知道该传什么 |
|
|
107
110
|
| `tool-undeclared-param` | `execute` 读了 `args.x`,但 `parameters.properties` 里没有 `x`。模型只能传 schema 声明过的参数,所以这个值在真实调用里永远是 `undefined`——代码在,永远不跑,也不报错。`args.x`、`args['x']`、`const { x } = args` 三种写法都认 |
|
|
108
111
|
| `tool-output-invalid` | 工具返回的东西**自己的 output schema 不收**。宿主每个结果都拿 schema 校验,不匹配就抛 `ToolOutputError`——工具从第一次调用起就不可用,而报错只给一个 JSON 路径,不给是哪行代码返回的。只有点名 `call` 真的调了才检查。这个插件自己踩过:schema 收紧成 `additionalProperties: false`,但有一条错误分支还在返回 `{ ok, error }` |
|
|
109
112
|
| `tool-input-throws` | 把每个声明过的参数**塞一个错类型的值**去调,工具直接抛 `TypeError` 而不是回答。宿主从不拿 `parameters` 校验入参,所以这个值真的会到你的代码里——调用方拿到的是 `The "path" argument must be of type string`,插件日志里没有一行说是哪个工具。这条规则是在自己的工作区里跑出 **12 个真实崩溃** 之后才加的(`session`/`profile`/`casePath`/`workspace` 各一个入口),现在九个插件全绿。默认关:它会把每个工具都调一遍,用 `skip` 点名排除那些会开程序、写文件的 |
|
|
@@ -124,6 +127,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
124
127
|
| `client-apply-throws` | `apply(ctx)` 抛 |
|
|
125
128
|
| `client-no-registration` | `apply` 跑完一次服务都没调用——面板不会出现 |
|
|
126
129
|
| `client-inject-missing-<service>` | `apply` 用了 `ctx.sidebarRightTabs` 但 `inject` 数组里没写它。cordis 不会把没声明的服务给你,于是 `ctx.sidebarRightTabs` 是 `undefined`,`apply` 一碰就抛(或静默什么都没注册) |
|
|
130
|
+
| `client-double-load` | 同一个 bundle 被注入了两次(它自己的 patch 一次,注入 `dsh-client-modules` 的侧边栏插件再一次)。loader 按 id 建工厂,第二次注册直接抛错,整个 Web 启动被中断——不是这个面板没了,是所有面板都没了。用 `if (window.__xLoaded) return; window.__xLoaded = true` 挡一下 |
|
|
127
131
|
| `client-parse-failed` | 文件有语法错误。**单独判**:`SyntaxError` 说明 bundle 本身是坏的,别的报错说明是沙箱不够真,两者别混为一谈 |
|
|
128
132
|
| `client-unavailable` | 子进程没跑起来(warn)——这一层没跑,别把它当干净 |
|
|
129
133
|
|
|
@@ -216,7 +220,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
216
220
|
## 开发
|
|
217
221
|
|
|
218
222
|
```sh
|
|
219
|
-
node --test #
|
|
223
|
+
node --test # 102 个引擎单测
|
|
220
224
|
node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
|
|
221
225
|
```
|
|
222
226
|
|
package/lib/engine.js
CHANGED
|
@@ -1201,11 +1201,8 @@ const walkParams = (schema, path) => {
|
|
|
1201
1201
|
const ARG_DOT = /\\bargs\\s*\\??\\.\\s*([A-Za-z_$][\\w$]*)/g
|
|
1202
1202
|
const ARG_INDEX = /\\bargs\\s*\\[\\s*['"]([^'"]+)['"]\\s*\\]/g
|
|
1203
1203
|
const ARG_DESTRUCTURE = /\\{\\s*([^{}]+?)\\s*\\}\\s*=\\s*args\\b/g
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
if (!props || typeof props !== 'object') return []
|
|
1207
|
-
const declared = Object.keys(props)
|
|
1208
|
-
if (typeof def.execute !== 'function') return []
|
|
1204
|
+
const argReads = (def) => {
|
|
1205
|
+
if (typeof def?.execute !== 'function') return []
|
|
1209
1206
|
let src = ''
|
|
1210
1207
|
try { src = def.execute.toString() } catch { return [] }
|
|
1211
1208
|
const reads = []
|
|
@@ -1217,7 +1214,13 @@ const undeclaredArgs = (def) => {
|
|
|
1217
1214
|
if (/^[A-Za-z_$][\\w$]*$/.test(key)) reads.push(key)
|
|
1218
1215
|
}
|
|
1219
1216
|
}
|
|
1220
|
-
return [...new Set(reads)]
|
|
1217
|
+
return [...new Set(reads)]
|
|
1218
|
+
}
|
|
1219
|
+
const undeclaredArgs = (def) => {
|
|
1220
|
+
const props = def?.parameters?.properties
|
|
1221
|
+
if (!props || typeof props !== 'object') return []
|
|
1222
|
+
const declared = Object.keys(props)
|
|
1223
|
+
return argReads(def).filter((r) => !declared.includes(r))
|
|
1221
1224
|
}
|
|
1222
1225
|
const noopService = () => new Proxy(function () {}, {
|
|
1223
1226
|
get: (target, prop) => {
|
|
@@ -1249,6 +1252,8 @@ ctx.tools = {
|
|
|
1249
1252
|
hasOutputSchema: Boolean(def && def.output && def.output.schema),
|
|
1250
1253
|
schema: (def && def.output && def.output.schema) || null,
|
|
1251
1254
|
parameters: (def && def.parameters) || null,
|
|
1255
|
+
description: typeof def?.description === 'string' ? def.description : '',
|
|
1256
|
+
argReads: argReads(def),
|
|
1252
1257
|
cardKind: card,
|
|
1253
1258
|
undeclaredParams: undeclaredArgs(def),
|
|
1254
1259
|
run: typeof def?.execute === 'function' ? def.execute : null,
|
|
@@ -1477,7 +1482,47 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1477
1482
|
else seen.add(t.name)
|
|
1478
1483
|
|
|
1479
1484
|
if (!t.hasExecute) out.push(F('tool-missing-execute', 'error', `tool "${label}" has no execute function`, "It appears in the model's tool list and throws the moment it is called.", 'Add execute(args, exec).'))
|
|
1480
|
-
|
|
1485
|
+
|
|
1486
|
+
// The description is the only thing that tells the model what the tool is
|
|
1487
|
+
// for. Without one the tool is a name in a list, and a name is not a
|
|
1488
|
+
// specification: it gets called for the wrong thing or not at all.
|
|
1489
|
+
const desc = typeof t.description === 'string' ? t.description.trim() : ''
|
|
1490
|
+
if (!desc) {
|
|
1491
|
+
out.push(
|
|
1492
|
+
F(
|
|
1493
|
+
'tool-no-description',
|
|
1494
|
+
'error',
|
|
1495
|
+
`tool "${label}" has no description`,
|
|
1496
|
+
"The model chooses tools from name + description + parameters. With no description it has to guess from the name alone, so the tool is shipped but effectively unselectable.",
|
|
1497
|
+
'Write one sentence: what it reads, and what it returns.',
|
|
1498
|
+
),
|
|
1499
|
+
)
|
|
1500
|
+
} else if (desc.length < 30 || desc.toLowerCase() === String(label).toLowerCase()) {
|
|
1501
|
+
out.push(
|
|
1502
|
+
F(
|
|
1503
|
+
'tool-description-thin',
|
|
1504
|
+
'warn',
|
|
1505
|
+
`tool "${label}" has a ${desc.length}-character description`,
|
|
1506
|
+
'A description that only repeats the name adds nothing the model did not already have, so it picks between tools on vibes.',
|
|
1507
|
+
'Say what it takes and what it gives back, in one sentence.',
|
|
1508
|
+
),
|
|
1509
|
+
)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// A tool that reads args but declares no parameters schema: the model is
|
|
1513
|
+
// given no argument list at all, so every field it must send is invisible.
|
|
1514
|
+
if (!t.parameters && Array.isArray(t.argReads) && t.argReads.length > 0) {
|
|
1515
|
+
out.push(
|
|
1516
|
+
F(
|
|
1517
|
+
'tool-no-parameters',
|
|
1518
|
+
'error',
|
|
1519
|
+
`tool "${label}" reads args.${t.argReads[0]} but declares no parameters schema`,
|
|
1520
|
+
'The host renders parameters into the prompt. With none, the model is never told which arguments exist, so it cannot send them.',
|
|
1521
|
+
'Declare parameters: { type: "object", properties: { … } }.',
|
|
1522
|
+
),
|
|
1523
|
+
)
|
|
1524
|
+
}
|
|
1525
|
+
if (!t.hasOutputSchema) out.push(F('tool-missing-output-schema', 'tool-no-description', 'tool-description-thin', 'tool-no-parameters', 'warn', `tool "${label}" declares no output.schema`, 'The host validates tool output against output.schema; without one the contract is implicit and a malformed result reaches the caller.', "Add output: { schema: { type: 'json' }, render }."))
|
|
1481
1526
|
if (t.cardKind && typeof t.cardKind === 'object') {
|
|
1482
1527
|
out.push(F('tool-present-call-throws', 'error', `tool "${label}" presentCall() throws`, `presentCall renders the transcript card and must be total: ${t.cardKind.threw}`, 'Do not read args fields that may be absent.'))
|
|
1483
1528
|
} else if (typeof t.cardKind === 'string' && !CARD_KINDS.has(t.cardKind)) {
|
|
@@ -1786,6 +1831,24 @@ if (!records.parseError) {
|
|
|
1786
1831
|
else records.evalError = text
|
|
1787
1832
|
}
|
|
1788
1833
|
}
|
|
1834
|
+
// The same bundle can reach the page twice: its own patch inserts it, and any
|
|
1835
|
+
// sidebar-style plugin that injects dsh-client-modules inserts it again. The
|
|
1836
|
+
// loader keys factories by id and throws on the second registration, which
|
|
1837
|
+
// aborts web boot. Evaluate a second time against the same window and see
|
|
1838
|
+
// whether load() is called again; a window-level guard makes it a no-op.
|
|
1839
|
+
records.doubleLoad = null
|
|
1840
|
+
if (!records.parseError && records.loaded > 0) {
|
|
1841
|
+
let second = 0
|
|
1842
|
+
const first = globalThis.window.__ModuleLoader__.load
|
|
1843
|
+
globalThis.window.__ModuleLoader__.load = (spec) => {
|
|
1844
|
+
second += 1
|
|
1845
|
+
throw new Error('duplicate factory registration: ' + String(spec && spec.id))
|
|
1846
|
+
}
|
|
1847
|
+
try { new Function(source)() } catch (e) {
|
|
1848
|
+
records.doubleLoad = second > 0 ? String(e && e.message || e) : null
|
|
1849
|
+
}
|
|
1850
|
+
globalThis.window.__ModuleLoader__.load = first
|
|
1851
|
+
}
|
|
1789
1852
|
process.stdout.write('\\n@@PG@@' + JSON.stringify(records))
|
|
1790
1853
|
process.exit(0)
|
|
1791
1854
|
`
|
|
@@ -1847,6 +1910,17 @@ export async function checkClientRuntime(dir, opts = {}) {
|
|
|
1847
1910
|
if (r.applyError) {
|
|
1848
1911
|
out.push(F('client-apply-throws', 'error', `apply() in ${target} throws: ${String(r.applyError).slice(0, 160)}`, 'Registration happens inside apply(); a throw there leaves the panel unregistered while the host half keeps working.', 'Wrap the failing registration, or fix the cause.'))
|
|
1849
1912
|
}
|
|
1913
|
+
if (r.doubleLoad) {
|
|
1914
|
+
out.push(
|
|
1915
|
+
F(
|
|
1916
|
+
'client-double-load',
|
|
1917
|
+
'warn',
|
|
1918
|
+
`${target} registers a factory again when evaluated a second time (${String(r.doubleLoad).slice(0, 120)})`,
|
|
1919
|
+
'A client bundle can reach the page twice: its own patch inserts it, and any sidebar-style plugin that injects dsh-client-modules inserts it again. The loader keys factories by id and throws on the second registration, which aborts web boot — the whole panel disappears, not just this one.',
|
|
1920
|
+
'Guard the load call with a window-level flag: if (window.__myPluginLoaded) return; window.__myPluginLoaded = true.',
|
|
1921
|
+
),
|
|
1922
|
+
)
|
|
1923
|
+
}
|
|
1850
1924
|
if (r.hasApply && !r.applyError && r.registrations.length === 0) {
|
|
1851
1925
|
out.push(
|
|
1852
1926
|
F(
|
|
@@ -2234,7 +2308,7 @@ const RULE_ORDER = [
|
|
|
2234
2308
|
'missing-field', 'not-esm', 'files-omits-lib', 'files-omits-patch', 'no-exports', 'client-without-export', 'client-without-platform',
|
|
2235
2309
|
'entry-path-missing', 'bundle-patch-not-declared', 'no-publish-config-access', 'private-field-set', 'publish-config-registry',
|
|
2236
2310
|
'client-file-missing', 'client-no-module-loader', 'client-parse-failed', 'client-load-not-called',
|
|
2237
|
-
'client-factory-throws', 'client-no-apply', 'client-apply-throws', 'client-no-registration',
|
|
2311
|
+
'client-factory-throws', 'client-no-apply', 'client-apply-throws', 'client-no-registration', 'client-double-load',
|
|
2238
2312
|
'client-inject-missing-slots', 'client-inject-missing-sidebarRightTabs', 'client-unavailable',
|
|
2239
2313
|
'no-patch-file', 'no-insert-row', 'insert-missing-name', 'insert-name-mismatch',
|
|
2240
2314
|
'patch-name-unresolvable', 'no-name-export', 'no-apply-export', 'no-inject', 'require-in-esm',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness — plus an input fuzzer that drives every declared parameter with the wrong type. 发布前的七层门禁 + 入参模糊测试。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|