// 回归:mail-bridge 的工具 schema 必须是合法 JSON Schema。 // // 曾经这里用裸 `require(...)` 载入 @deepseek-ai/dsh-tools。插件是 ESM, // ESM 里没有 require → 抛 ReferenceError → 被 `catch { return opts; }` 静默吞掉, // 于是 defineTool 恒等返回,工具的 parameters 以**未编译的裸映射**注册: // // { gateway_url: {type:'string'}, key_token: {type:'string'} } // // 而不是: // // { type:'object', properties:{ gateway_url:…, key_token:… } } // // 严格的上游直接 400(实测 OpenCode Go): // Invalid schema for function 'connect_to_server': // schema must be a JSON Schema of 'type: "object"', got 'type: null'. // 宽松的上游(Claude 系)不校验,所以只在特定 AUTO 档位暴露。 import { test } from 'node:test'; import assert from 'node:assert/strict'; import { parametersToJsonSchema } from '../dist/index.js'; test('裸参数映射被编译成 type:object + properties', () => { const got = parametersToJsonSchema({ gateway_url: { type: 'string', description: 'Gateway 地址' }, key_token: { type: 'string', description: '密钥' }, }); assert.equal(got.type, 'object', '缺 type:object 会被严格上游 400'); assert.deepEqual(Object.keys(got.properties), ['gateway_url', 'key_token']); // 属性必须原样保留 assert.equal(got.properties.gateway_url.type, 'string'); // 没有任何 required 时不应凭空产出空数组 assert.equal('required' in got, false); }); test('required:true 被提升到根级 required 数组,并从属性上移除', () => { const got = parametersToJsonSchema({ to: { type: 'string', required: true, description: '收件人' }, cc: { type: 'string', description: '抄送' }, }); assert.deepEqual(got.required, ['to']); // JSON Schema 的属性里不允许出现 required assert.equal('required' in got.properties.to, false); assert.equal(got.properties.to.type, 'string'); }); test('数组属性与其 items 原样保留', () => { const got = parametersToJsonSchema({ attachment_ids: { type: 'array', items: { type: 'string' }, description: '附件' }, }); assert.deepEqual(got.properties.attachment_ids.items, { type: 'string' }); }); test('空 / 缺失 spec 产出合法的空对象 schema', () => { for (const spec of [undefined, null, {}]) { const got = parametersToJsonSchema(spec); assert.equal(got.type, 'object'); assert.deepEqual(got.properties, {}); } }); test('编译结果本身通过「必须有 type:object」的断言(上游的实际要求)', () => { // 真机上被拒的那个工具 const connect = parametersToJsonSchema({ gateway_url: { type: 'string', description: 'Gateway 地址,如 https://mail.example.com;省略则用当前配置' }, key_token: { type: 'string', description: '管理员签发的 Agent 密钥;省略则用本地密钥(不存在时自动生成)' }, }); assert.equal(connect.type, 'object'); // 模拟上游校验:type 必须存在且为 object assert.ok(connect.type === 'object', `got type: ${JSON.stringify(connect.type ?? null)}`); });