export const DEFAULT_DESCRIPTOR = { version: 1, device: { id: "cabin_refrigerator", name: "车载冰箱", aliases: ["冰箱", "车载冷藏箱", "冷藏箱"], domain: "refrigerator", category: "vehicle_electrical", targetDevice: "refrigerator", targetScopes: ["vehicle"], defaultScope: "vehicle", transport: "native_can", channel: "实验通道", }, readouts: [ { key: "power", label: "工作状态", value: "制冷中" }, { key: "temperature", label: "当前温度", value: "6℃" }, { key: "mode", label: "当前模式", value: "节能" }, ], capabilities: [ { id: "refrigerator.set_power", label: "工作状态", property: "refrigerator.power", operation: "set", enabled: true, risk: "low", confirmationPolicy: "on_ambiguity", parameters: [ { name: "state", type: "string", required: true, allowedValues: ["on", "off"] }, ], intents: [ { label: "开启", phrases: ["打开", "开启", "启动", "开启制冷"], values: { state: "on" } }, { label: "关闭", phrases: ["关闭", "关掉", "停止", "停止制冷"], values: { state: "off" } }, ], ui: { type: "select", value: "on", options: [["off", "关闭"], ["on", "开启"]], }, executor: { binding: "NativeCanRuntime", mappingRef: "demo.refrigerator.power" }, }, { id: "refrigerator.set_temperature", label: "目标温度", property: "refrigerator.temperature", operation: "set", enabled: true, risk: "medium", confirmationPolicy: "on_ambiguity", parameters: [ { name: "temperature_c", type: "number", unit: "celsius", required: true, minimum: 0, maximum: 15 }, ], intents: [ { label: "设置温度", phrases: ["温度", "调到", "设置为", "制冷到", "冷藏到"], extract: { parameter: "temperature_c", kind: "number" }, }, ], ui: { type: "range", value: 4, minimum: 0, maximum: 15, step: 1, unit: "℃" }, executor: { binding: "NativeCanRuntime", mappingRef: "demo.refrigerator.temperature" }, }, { id: "refrigerator.set_mode", label: "工作模式", property: "refrigerator.mode", operation: "set", enabled: true, risk: "low", confirmationPolicy: "on_ambiguity", parameters: [ { name: "mode", type: "string", required: true, allowedValues: ["eco", "fast_cool"] }, ], intents: [ { label: "节能", phrases: ["节能模式", "省电模式", "节能"], values: { mode: "eco" } }, { label: "速冷", phrases: ["速冷模式", "快速制冷", "速冷"], values: { mode: "fast_cool" } }, ], ui: { type: "select", value: "eco", options: [["eco", "节能"], ["fast_cool", "速冷"]], }, executor: { binding: "NativeCanRuntime", mappingRef: "demo.refrigerator.mode" }, }, ], }; const SCOPE_ALIASES = { driver: ["主驾", "驾驶位", "司机位"], passenger: ["副驾", "副驾驶", "乘客位"], rear_row: ["后排", "第二排"], vehicle: ["全车", "整车", "车内"], }; const POLITE_WORDS = /^(?:请|麻烦|拜托|帮我|请帮我|给我)+/; const END_POLITE_WORDS = /(?:谢谢|可以吗|好吗|行吗)[。!!??]*$/; export function cloneDescriptor(value = DEFAULT_DESCRIPTOR) { return JSON.parse(JSON.stringify(value)); } export function splitAliases(value) { return [...new Set(String(value || "") .split(/[,,、;;|]/) .map((item) => item.trim()) .filter(Boolean))]; } export function normalizeText(value) { return String(value || "") .normalize("NFKC") .trim() .replace(POLITE_WORDS, "") .replace(END_POLITE_WORDS, "") .toLowerCase() .replace(/[\s,,。;;!!??、]/g, ""); } function longestContained(normalizedText, phrases) { return [...new Set(phrases.map(normalizeText).filter(Boolean))] .filter((phrase) => normalizedText.includes(phrase)) .sort((left, right) => right.length - left.length)[0] || ""; } function chineseInteger(token) { if (!token) return null; if (/^\d+$/.test(token)) return Number(token); const digits = { 零: 0, 〇: 0, 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9 }; if (token.includes("十")) { const [left, right] = token.split("十", 2); const tens = left ? digits[left.at(-1)] : 1; const ones = right ? digits[right.at(-1)] : 0; return Number.isFinite(tens) && Number.isFinite(ones) ? tens * 10 + ones : null; } if ([...token].every((char) => Object.hasOwn(digits, char))) { return Number([...token].map((char) => digits[char]).join("")); } return null; } function extractNumber(text) { const belowZero = String(text).match(/零下(\d+(?:\.\d+)?|[零〇一二两三四五六七八九十]+)/); if (belowZero) { const value = /^\d/.test(belowZero[1]) ? Number(belowZero[1]) : chineseInteger(belowZero[1]); return value === null ? null : -value; } const numeric = String(text).match(/-?\d+(?:\.\d+)?/); if (numeric) return Number(numeric[0]); const chinese = String(text).match(/[零〇一二两三四五六七八九十]+/); return chinese ? chineseInteger(chinese[0]) : null; } function resolveScope(normalizedText, device) { const explicit = Object.entries(SCOPE_ALIASES) .flatMap(([scope, aliases]) => aliases.map((alias) => ({ scope, alias: normalizeText(alias) }))) .find(({ alias }) => normalizedText.includes(alias)); if (explicit) { if (!device.targetScopes.includes(explicit.scope)) { return { status: "unsupported", scope: explicit.scope, explicit: true, reason: `设备不支持“${explicit.alias}”范围` }; } return { status: "resolved", scope: explicit.scope, explicit: true, reason: `显式目标:${explicit.alias}` }; } if (device.defaultScope && device.targetScopes.includes(device.defaultScope)) { return { status: "resolved", scope: device.defaultScope, explicit: false, reason: `采用卡片默认目标:${device.defaultScope}` }; } if (device.targetScopes.length === 1) { return { status: "resolved", scope: device.targetScopes[0], explicit: false, reason: `能力仅支持唯一目标:${device.targetScopes[0]}` }; } return { status: "ambiguous", scope: null, explicit: false, reason: "存在多个目标范围,需要用户确认" }; } function validateParameters(capability, parameters) { const issues = []; for (const schema of capability.parameters || []) { const value = parameters[schema.name]; if ((value === undefined || value === null || value === "") && schema.required) { issues.push(`缺少必填参数 ${schema.name}`); continue; } if (value === undefined || value === null || value === "") continue; if (schema.type === "number" && typeof value !== "number") { issues.push(`${schema.name} 必须是数字`); } if (schema.minimum !== undefined && value < schema.minimum) { issues.push(`${schema.name}=${value} 低于下限 ${schema.minimum}`); } if (schema.maximum !== undefined && value > schema.maximum) { issues.push(`${schema.name}=${value} 超过上限 ${schema.maximum}`); } if (schema.allowedValues && !schema.allowedValues.includes(value)) { issues.push(`${schema.name}=${value} 不在允许值中`); } } return issues; } function renderCleanText(descriptor, capability, parameters) { const deviceName = descriptor.device.name; if (capability.property.endsWith(".power")) { return `${parameters.state === "on" ? "开启" : "关闭"}${deviceName}。`; } if (capability.property.endsWith(".temperature")) { return `将${deviceName}设置为${parameters.temperature_c}度。`; } if (capability.property.endsWith(".mode")) { const modeLabels = { eco: "节能模式", fast_cool: "速冷模式" }; return `将${deviceName}设置为${modeLabels[parameters.mode] || parameters.mode}。`; } return `执行${deviceName}的${capability.label}。`; } function baseResult(rawText, normalizedText, trace) { return { raw_text: rawText, normalized_text: normalizedText, semantic_status: "no_command", execution_eligible: false, clean_text: "", command: null, issues: [], trace, }; } export function parseInstruction(rawText, descriptorInput = DEFAULT_DESCRIPTOR) { const descriptor = cloneDescriptor(descriptorInput); const normalizedText = normalizeText(rawText); const trace = [{ stage: "文本整理", state: "success", detail: normalizedText || "输入为空" }]; const result = baseResult(String(rawText || ""), normalizedText, trace); if (!normalizedText) { result.semantic_status = "incomplete"; result.issues.push("请输入自然语言指令"); trace.push({ stage: "设备识别", state: "error", detail: "没有可分析的文字" }); return result; } const devicePhrases = [descriptor.device.name, ...(descriptor.device.aliases || [])]; const matchedDevice = longestContained(normalizedText, devicePhrases); if (!matchedDevice) { result.issues.push("指令没有命中设备名称或别名"); trace.push({ stage: "设备识别", state: "error", detail: `候选:${devicePhrases.join("、")}` }); return result; } trace.push({ stage: "设备识别", state: "success", detail: `“${matchedDevice}” → ${descriptor.device.id}` }); const candidates = []; for (const capability of descriptor.capabilities.filter((item) => item.enabled !== false)) { for (const intent of capability.intents || []) { const phrase = longestContained(normalizedText, intent.phrases || []); if (!phrase) continue; const parameters = { ...(intent.values || {}) }; if (intent.extract?.kind === "number") { const number = extractNumber(rawText); if (number !== null) parameters[intent.extract.parameter] = number; } candidates.push({ capability, intent, phrase, parameters, score: matchedDevice.length * 100 + phrase.length, }); } } if (!candidates.length) { result.semantic_status = "incomplete"; result.issues.push("设备已识别,但没有命中已启用的功能说法"); trace.push({ stage: "功能识别", state: "error", detail: "请使用卡片中定义的功能说法" }); return result; } candidates.sort((left, right) => right.score - left.score); const best = candidates[0]; const tied = candidates.filter((item) => item.score === best.score); if (tied.length > 1) { result.semantic_status = "ambiguous"; result.issues.push(`同时命中多个功能:${tied.map((item) => item.intent.label).join("、")}`); trace.push({ stage: "功能识别", state: "error", detail: "最高匹配分相同,需要补充更明确的动作" }); return result; } trace.push({ stage: "功能识别", state: "success", detail: `“${best.phrase}” → ${best.capability.id}` }); const scope = resolveScope(normalizedText, descriptor.device); trace.push({ stage: "目标解析", state: scope.status === "resolved" ? "success" : "error", detail: scope.reason }); if (scope.status !== "resolved") { result.semantic_status = scope.status === "ambiguous" ? "ambiguous" : "recognized_unsupported"; result.issues.push(scope.reason); return result; } const issues = validateParameters(best.capability, best.parameters); trace.push({ stage: "参数校验", state: issues.length ? "error" : "success", detail: issues.length ? issues.join(";") : JSON.stringify(best.parameters), }); if (issues.length) { result.semantic_status = Object.keys(best.parameters).length ? "recognized_unsupported" : "incomplete"; result.issues.push(...issues); return result; } result.semantic_status = "recognized_executable"; result.execution_eligible = true; result.clean_text = renderCleanText(descriptor, best.capability, best.parameters); result.command = { domain: descriptor.device.domain, capability: best.capability.id, property: best.capability.property, target_device: descriptor.device.targetDevice, target_scope: scope.scope, operation: best.capability.operation, parameters: best.parameters, risk_level: best.capability.risk, confirmation_policy: best.capability.confirmationPolicy, executor_binding: best.capability.executor.binding, executor_payload: { transport: descriptor.device.transport, mapping_ref: best.capability.executor.mappingRef, capability: best.capability.id, target: scope.scope, parameters: best.parameters, demo_only: true, }, }; trace.push({ stage: "执行绑定", state: "success", detail: `${best.capability.executor.binding} · 演示载荷,不发送` }); return result; } export function buildManualCommand(descriptorInput, values) { const descriptor = cloneDescriptor(descriptorInput); const commands = []; for (const capability of descriptor.capabilities.filter((item) => item.enabled !== false)) { const value = values[capability.id]; if (value === undefined) continue; const schema = capability.parameters?.[0]; if (!schema) continue; const parsedValue = schema.type === "number" ? Number(value) : value; const parameters = { [schema.name]: parsedValue }; const issues = validateParameters(capability, parameters); if (issues.length) continue; commands.push({ capability: capability.id, target_scope: descriptor.device.defaultScope, parameters, demo_only: true, }); } return commands; }