@lynn123411/dsh-ask-user-grilling 0.6.3 → 0.7.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/lib/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.js", "../src/contract.js", "../src/recommendation.js"],
4
- "sourcesContent": ["import { defineTool } from \"@deepseek-ai/dsh-tools\";\nimport \"@deepseek-ai/dsh-user-questions\";\nimport { RESERVED_ID_PREFIX, ROUND_END_QUESTION, isBlank, mergeNumberIntoHeader } from \"./contract.js\";\nimport { normalizeOption } from \"./recommendation.js\";\n\n/**\n * @lynn123411/dsh-ask-user-grilling \u2014 a presentation variant of the native\n * `ask_user_question` (`@deepseek-ai/dsh-tool-ask-user`). Same capability seam\n * (ctx.userQuestions), the same tool description and the same descriptions for\n * the shared parameters verbatim; only the rendered form differs.\n *\n * ask_user_grilling:\n * - forces multi-select on every question \u2014 the schema offers no opt-out\n * - merges the optional `number` into the header as \"<number> \u00B7 <header>\",\n * so a form page carries the same Q-number the round announced in prose;\n * a header that already opens with that number is sent as it is\n * - passes the optional `detail` through to the seam: the client renders it as\n * markdown under the question, while the question itself is a plain heading\n * with no markdown and no line breaks, so a multi-paragraph body belongs in\n * `detail`\n * - normalizes the recommendation marker into the one form the client renders,\n * a trailing \"\uFF08\u63A8\u8350\uFF09\" on the label: an explicit `recommended` flag, or a\n * loose marker at the end of the label or the description, is moved there \u2014\n * the client reads only the label and accepts only the bracketed suffix\n * - appends a round-end supplement question; per-question supplement goes\n * through the built-in custom input (\"Type your answer\" / \"\u8F93\u5165\u4F60\u7684\u7B54\u6848\"),\n * so no extra per-question option is added (it would duplicate that field)\n * - rejects what the form cannot carry: a question id under the reserved\n * `__grill_` prefix, a duplicate question id, a blank question text, a blank\n * option label, or two options of one question that end up with the same\n * label (the client keys selection by label, so a collision cannot be\n * restored or even clicked apart)\n *\n * Two rows mount this module. The preset tool row registers the tool (the\n * default). The profile bundle row, inserted by this package's\n * `cordis.patch.yml`, exists only so the Web client serves `lib/client.js` \u2014\n * the browser bundle is served for an enabled Loader entry, and a preset row is\n * a directly plugged subtree rather than a Loader entry, so the package must be\n * in `dsh.profile.bundles` for its card to reach the page. That row passes\n * `config.carrier: true` and registers nothing; without it the tool would also\n * become a root-layer tool, visible to agents on every other preset.\n */\nconst name = \"tool-ask-user-grilling\";\nconst inject = [\"tools\", \"userQuestions\"];\n\n/** \u8F7D\u4F53\u884C\u8BA4\u8FD9\u4E00\u4E2A\u952E\uFF0C\u8BA4\u5230\u5C31\u6574\u884C\u4EC0\u4E48\u90FD\u4E0D\u6CE8\u518C\uFF08\u6D4F\u89C8\u5668\u534A\u8FB9\u7684 servable entry\uFF0C\u89C1\u6587\u4EF6\u5934\uFF09\u3002 */\nconst CONFIG_KEY = \"carrier\";\n\n/**\n * \u8BFB\u8FD9\u4E00\u884C\u7684 config\u3002\u672A\u77E5\u952E\u4E0E\u9519\u7C7B\u578B\u5F53\u573A\u629B\uFF1A\u62FC\u9519\u7684\u8F7D\u4F53\u884C\u4F1A\u9759\u9ED8\u591A\u6CE8\u518C\u4E00\u4E2A\u5168\u5C40\u5DE5\u5177\uFF0C\n * \u90A3\u6BD4\u542F\u52A8\u5931\u8D25\u96BE\u67E5\u5F97\u591A\u3002\n * @param {unknown} config - loader \u4EA4\u7ED9\u63D2\u4EF6\u884C\u7684 config\u3002\n * @returns {{ carrier: boolean }} \u5F52\u4E00\u5316\u540E\u7684\u914D\u7F6E\u3002\n */\nfunction readConfig(config) {\n if (config === undefined || config === null) return { carrier: false };\n if (typeof config !== \"object\" || Array.isArray(config)) {\n throw new TypeError(`${name}: config must be an object with an optional boolean \"${CONFIG_KEY}\"`);\n }\n const unknown = Object.keys(config).filter((key) => key !== CONFIG_KEY);\n if (unknown.length > 0) {\n throw new TypeError(`${name}: unknown config key(s) ${unknown.map((key) => JSON.stringify(key)).join(\", \")}; the only accepted key is \"${CONFIG_KEY}\"`);\n }\n if (config[CONFIG_KEY] !== undefined && typeof config[CONFIG_KEY] !== \"boolean\") {\n throw new TypeError(`${name}: config.${CONFIG_KEY} must be a boolean`);\n }\n return { carrier: config[CONFIG_KEY] === true };\n}\n\nfunction apply(ctx, config) {\n if (readConfig(config).carrier) return;\n ctx.tools.register(defineTool({\n name: \"ask_user_grilling\",\n description: \"Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding.\",\n parameters: {\n questions: {\n type: \"array\",\n required: true,\n description: \"Questions to ask the user before continuing.\",\n items: {\n type: \"object\",\n additionalProperties: true,\n properties: {\n id: {\n type: \"string\",\n required: true,\n description: \"Stable id for this question; echoed in the answer.\",\n },\n question: {\n type: \"string\",\n required: true,\n description: \"The specific question to ask the user.\",\n },\n header: {\n type: \"string\",\n description: \"Optional short heading for the question, such as \\\"Confirm\\\" or \\\"Choose Mode\\\".\",\n },\n number: {\n type: \"string\",\n description: \"Optional question number, e.g. \\\"Q2\\\". Rendered before the header as \\\"Q2 \u00B7 <header>\\\"; use the same Q-number you announced in the message text.\",\n },\n detail: {\n type: \"string\",\n description: \"Optional supporting body for this question. Rendered as markdown under the question, so put multi-paragraph context here and keep question to the one line that asks.\",\n },\n options: {\n type: \"array\",\n description: \"Optional choices to show the user. If you recommend one, put it first and append \\\"(Recommended)\\\" to that label.\",\n items: {\n type: \"object\",\n additionalProperties: true,\n properties: {\n label: {\n type: \"string\",\n required: true,\n description: \"Short user-facing option label.\",\n },\n description: {\n type: \"string\",\n description: \"One sentence explaining the tradeoff or impact.\",\n },\n recommended: {\n type: \"boolean\",\n description: \"Set true on the option you recommend (alternative to the \\\"(Recommended)\\\" label suffix).\",\n },\n },\n },\n },\n },\n },\n },\n },\n output: {\n schema: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n rejected: {\n type: \"boolean\",\n description: \"True when input validation failed and the round was not asked.\",\n },\n violations: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Validation violations when rejected.\",\n },\n error: {\n type: \"string\",\n description: \"Human-readable error when rejected.\",\n },\n answers: {\n type: \"array\",\n required: true,\n description: \"One entry per question, in the order asked.\",\n items: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n id: {\n type: \"string\",\n required: true,\n description: \"The question id you supplied.\",\n },\n selected: {\n type: \"array\",\n required: true,\n items: { type: \"string\" },\n description: \"Labels of the options the user picked (may be empty if skipped).\",\n },\n custom: {\n type: \"string\",\n description: \"User-typed free text for this question, if any.\",\n },\n },\n },\n },\n },\n },\n render: (_args, value) => [{ type: \"text\", text: JSON.stringify(value) }],\n },\n async execute(args, exec) {\n // 1. input validation: only what the form cannot represent at all. Stem /\n // option separation stays guidance, NOT enforced: substring matching\n // would reject legitimate stems (e.g. a stem that naturally mentions an\n // option name), so no stem check may refuse a round \u2014 a bad stem is\n // preferable to a false rejection.\n const violations = [];\n const seenIds = new Set();\n args.questions.forEach((question, questionIndex) => {\n const where = `Question ${questionIndex + 1} (id ${JSON.stringify(question.id)})`;\n if (typeof question.id === \"string\" && question.id.startsWith(RESERVED_ID_PREFIX)) {\n violations.push(`${where} uses the reserved prefix ${RESERVED_ID_PREFIX} (owned by the round-end supplement question)`);\n }\n if (typeof question.id === \"string\") {\n if (seenIds.has(question.id)) violations.push(`${where} repeats an id already used in this round; ids must be unique because the answer is keyed by id`);\n seenIds.add(question.id);\n }\n if (isBlank(question.question)) {\n violations.push(`${where} has an empty question text; write the question itself in question and its body in detail`);\n }\n const seenLabels = new Set();\n (question.options ?? []).forEach((option, optionIndex) => {\n const label = normalizeOption(option).label;\n if (isBlank(label)) {\n violations.push(`${where}, option ${optionIndex + 1} has an empty label; every option needs a label the user can read and pick`);\n return;\n }\n if (seenLabels.has(label)) {\n violations.push(`${where} has two options that render with the same label ${JSON.stringify(label)}; the form identifies a choice by its label, so make them differ`);\n return;\n }\n seenLabels.add(label);\n });\n });\n if (violations.length > 0) {\n return {\n rejected: true,\n violations,\n error: `${violations.length} violation(s) in this round: ids must be unique and must not use the reserved ${RESERVED_ID_PREFIX} prefix, question text must not be blank, and the options of one question must not share a label. Fix them and call this tool again.`,\n answers: [],\n };\n }\n\n // 2. transform: force multi-select; merge the optional number into the\n // header; hand detail to the field the client renders as markdown; move\n // the recommendation marker to the label suffix the client renders;\n // per-question supplement is via the built-in custom input (\"Type your\n // answer\"/\"\u8F93\u5165\u4F60\u7684\u7B54\u6848\") \u2014 no extra option is added to avoid\n // duplication with that field\n const labelRestore = new Map();\n const questions = args.questions.map((question) => {\n const header = mergeNumberIntoHeader(question.number, question.header);\n const restore = new Map();\n const options = (question.options ?? []).map((option) => {\n const normalized = normalizeOption(option);\n if (normalized.label !== normalized.originalLabel) {\n restore.set(normalized.label, normalized.originalLabel);\n }\n return {\n label: normalized.label,\n ...(normalized.description !== undefined ? { description: normalized.description } : {}),\n };\n });\n labelRestore.set(question.id, restore);\n return {\n id: question.id,\n question: question.question,\n ...(header !== \"\" ? { header } : {}),\n ...(isBlank(question.detail) ? {} : { detail: question.detail }),\n options,\n multiSelect: true,\n };\n });\n\n // 3. round-end supplement question \u2014 single \"\u65E0\u9700\u8865\u5145\" option; supplement is via custom input, so no \"I have something to add\" option (duplicates that field)\n questions.push({\n ...ROUND_END_QUESTION,\n options: ROUND_END_QUESTION.options.map((option) => ({ ...option })),\n multiSelect: true,\n });\n\n // 4. ask through the userQuestions seam (UI renders from the service)\n const answer = await ctx.userQuestions.ask({\n questions,\n ...(exec.agent !== undefined ? { agent: exec.agent } : {}),\n signal: exec.signal,\n });\n return {\n answers: answer.answers.map((entry) => {\n const restore = labelRestore.get(entry.id);\n return {\n id: entry.id,\n selected: entry.selected.map((label) => restore?.get(label) ?? label),\n ...(entry.custom !== undefined ? { custom: entry.custom } : {}),\n };\n }),\n };\n },\n }));\n}\n\nexport { apply, inject, name };\n", "// \u5BBF\u4E3B\u534A\u8FB9\u4E0E\u754C\u9762\u534A\u8FB9\u5171\u7528\u7684\u7EAF\u5951\u7EA6\uFF1A\u9898\u76EE\u5B57\u6BB5\u7684\u62FC\u6CD5\uFF0C\u52A0\u8F6E\u672B\u8865\u5145\u9898\u8FD9\u4E00\u9053\u7531\u4EE3\u7801\u8FFD\u52A0\u7684\u9898\u3002\n//\n// \u4E24\u5904\u5FC5\u987B\u4E00\u81F4\uFF0C\u6240\u4EE5\u53EA\u6709\u8FD9\u4E00\u4EFD\uFF1A\u8868\u5355\u9875\u6807\u9898\u4E0E transcript \u5361\u7247\u6807\u9898\u90FD\u6309\n// mergeNumberIntoHeader \u62FC\uFF1B\u754C\u9762\u6309 id \u8BA4\u51FA\u8F6E\u672B\u8865\u5145\u9898\uFF0C\u4E5F\u6309 ROUND_END_QUESTION \u62FF\u5B83\u7684\u9898\u5E72\u3002\n// \u5404\u5199\u4E00\u4EFD\u5FC5\u7136\u6F02\u3002\n\n/** \u8F6E\u672B\u8865\u5145\u9898\u7684 id \u524D\u7F00\uFF0C\u540C\u65F6\u662F\u6A21\u578B\u4E0D\u5F97\u5360\u7528\u7684\u4FDD\u7559\u524D\u7F00\u3002 */\nexport const RESERVED_ID_PREFIX = '__grill_';\n\n/**\n * \u6BCF\u6B21\u8C03\u7528\u7531\u4EE3\u7801\u8FFD\u52A0\u5728\u672B\u5C3E\u7684\u8865\u5145\u9898\u3002\u754C\u9762\u534A\u8FB9\u6309 id \u8BA4\u51FA\u5B83\uFF0C\u5BBF\u4E3B\u534A\u8FB9\u628A\u5B83\u9001\u8FDB seam\u3002\n * \u9009\u9879\u6570\u7EC4\u7684\u6D45\u62F7\u8D1D\u7531\u5BBF\u4E3B\u5728\u6295\u9012\u524D\u505A\uFF08seam \u4FA7\u4E0D\u518D\u6539\u5B83\uFF0C\u4F46\u522B\u628A\u8FD9\u4EFD\u5E38\u91CF\u4EA4\u51FA\u53BB\uFF09\u3002\n */\nexport const ROUND_END_QUESTION = Object.freeze({\n id: `${RESERVED_ID_PREFIX}round_supplement__`,\n question: '\u8FD9\u8F6E\u8FD8\u6709\u4EC0\u4E48\u8981\u8865\u5145\u6216\u8C03\u6574\u7684\u5417\uFF1F',\n header: '\u8F6E\u672B\u8865\u5145',\n options: Object.freeze([Object.freeze({ label: '\u65E0\u9700\u8865\u5145' })]),\n});\n\n/**\n * \u53BB\u6389\u9996\u5C3E\u7A7A\u767D\u540E\u662F\u5426\u4E3A\u7A7A\u3002\u7A7A\u4E32\u4E0E\u7EAF\u7A7A\u767D\u90FD\u4E0D\u503C\u5F97\u9001\u8FDB\u8868\u5355\u3002\n * @param {unknown} value - \u5F85\u5224\u5B9A\u7684\u503C\u3002\n * @returns {boolean} \u975E\u5B57\u7B26\u4E32\u6216\u53BB\u7A7A\u767D\u540E\u4E3A\u7A7A\u65F6\u4E3A true\u3002\n */\nexport function isBlank(value) {\n return typeof value !== 'string' || value.trim() === '';\n}\n\n/**\n * \u628A\u9898\u53F7\u5E76\u8FDB\u6807\u9898\uFF1A\u4E24\u8005\u90FD\u7ED9\u5199\u4F5C `<number> \u00B7 <header>`\uFF0C\u53EA\u7ED9\u4E00\u4E2A\u5C31\u662F\u90A3\u4E00\u4E2A\uFF1B\n * \u6807\u9898\u5DF2\u7ECF\u4EE5\u8BE5\u9898\u53F7\u5F00\u5934\u65F6\u539F\u6837\u9001\u51FA\u53BB\uFF08\u6A21\u578B\u5E38\u81EA\u5DF1\u5199\u6210 \"Q2 \u00B7 Deadline\"\uFF09\u3002\n * @param {string} [number] - \u9898\u53F7\u3002\n * @param {string} [header] - \u6807\u9898\u3002\n * @returns {string} \u4EA4\u7ED9\u754C\u9762\u7684\u6807\u9898\uFF1B\u4E24\u8005\u90FD\u7F3A\u65F6\u4E3A\u7A7A\u4E32\uFF08\u8C03\u7528\u65B9\u636E\u6B64\u7701\u7565\u8BE5\u5B57\u6BB5\uFF09\u3002\n */\nexport function mergeNumberIntoHeader(number, header) {\n const num = isBlank(number) ? undefined : number;\n const head = isBlank(header) ? undefined : header;\n if (num === undefined) return head ?? '';\n if (head === undefined) return num;\n return head.startsWith(num) ? head : `${num} \u00B7 ${head}`;\n}\n", "// \u63A8\u8350\u6807\u8BB0\u7684\u5F52\u4E00\u5316\u3002\n//\n// \u5BA2\u6237\u7AEF\uFF08@deepseek-ai/dsh-client-ui-user-questions \u7684 parseRecommendedLabel\uFF09\n// \u53EA\u6709\u4E00\u4E2A\u5224\u5B9A\u4F9D\u636E\uFF1Aoption.label \u5FC5\u987B\u4EE5\u534A\u89D2\u6216\u5168\u89D2\u7684\u62EC\u53F7\u63A8\u8350\u5B57\u6837\u6536\u5C3E\uFF1Boption.description\n// \u5B8C\u5168\u4E0D\u53C2\u4E0E\u3002\u6A21\u578B\u7ECF\u5E38\u628A\u63A8\u8350\u5199\u6210 description \u672B\u5C3E\u7684\u300C\uFF1B\u63A8\u8350\u300D\uFF0C\u6216\u8005\u5199\u6210 label \u91CC\u4E0D\u5E26\n// \u62EC\u53F7\u7684\u5F62\u5F0F\uFF0C\u4E8E\u662F\u5FBD\u6807\u4E0D\u6E32\u67D3\u3001\u6807\u8BB0\u539F\u6837\u663E\u793A\u6210\u6587\u5B57\u3002\n//\n// \u8FD9\u91CC\u5728\u9898\u76EE\u4EA4\u7ED9 ctx.userQuestions.ask \u4E4B\u524D\u628A\u6807\u8BB0\u7EDF\u4E00\u642C\u5230 label \u672B\u5C3E\u7684\u300C\uFF08\u63A8\u8350\uFF09\u300D\uFF0C\n// \u5BA2\u6237\u7AEF\u5C31\u80FD\u8BA4\u51FA\u5E76\u6E32\u67D3\u5FBD\u6807\u3002\u663E\u5F0F\u5B57\u6BB5 recommended \u662F\u53E6\u4E00\u6761\u66F4\u660E\u786E\u7684\u4FE1\u53F7\uFF0C\u4F18\u5148\u4E8E\u6587\u5B57\u3002\n\nconst MARKER = 'recommended|\u63A8\u8350';\n// \u534A\u89D2/\u5168\u89D2\u5706\u62EC\u53F7\u4E0E\u65B9\u62EC\u53F7/\u65B9\u5934\u62EC\u53F7\nconst BRACKETED_MARKER = `[\uFF08(\u3010\\\\[]\\\\s*(?:${MARKER})\\\\s*[\uFF09)\u3011\\\\]]`;\n// \u65E0\u62EC\u53F7\u5199\u6CD5\u5FC5\u987B\u7531\u5206\u9694\u7B26\u5F15\u51FA\uFF0C\u5426\u5219\u300C\u67E5\u770B\u63A8\u8350\u300D\u8FD9\u79CD\u6B63\u5E38\u6587\u5B57\u4F1A\u88AB\u8BEF\u5224\nconst SEPARATOR = '\uFF1B;\uFF0C,\u3001:\uFF1A\\\\-\u2014';\nconst TRAILING_PUNCTUATION = '\u3002.\uFF01!\uFF1F?\uFF1B;\uFF0C,\u3001~\uFF5E\\\\s';\n\nconst BRACKETED_SUFFIX = new RegExp(`[\\\\s]*[${SEPARATOR}]?\\\\s*${BRACKETED_MARKER}[${TRAILING_PUNCTUATION}]*$`, 'i');\nconst BARE_SUFFIX = new RegExp(`[\\\\s]*[${SEPARATOR}]\\\\s*(?:${MARKER})[${TRAILING_PUNCTUATION}]*$`, 'i');\n\n/** \u4F9B\u6D4B\u8BD5\u6838\u5BF9\u5BA2\u6237\u7AEF\u89C4\u5219\u7684\u63A8\u8350\u6807\u8BB0\u540E\u7F00\uFF1B\u5BA2\u6237\u7AEF\u53EA\u8BA4 label \u672B\u5C3E\u7684\u8FD9\u4E24\u79CD\u62EC\u53F7\u5199\u6CD5\u3002 */\nexport const RECOMMENDED_LABEL_SUFFIX = '\uFF08\u63A8\u8350\uFF09';\n\n/**\n * \u53BB\u6389\u6587\u672C\u672B\u5C3E\u7684\u63A8\u8350\u6807\u8BB0\u3002\n * @param {string} text - label \u6216 description \u539F\u6587\u3002\n * @returns {{ text: string, found: boolean }} \u53BB\u6389\u6807\u8BB0\u540E\u7684\u6587\u672C\uFF0C\u4EE5\u53CA\u662F\u5426\u627E\u5230\u6807\u8BB0\u3002\n */\nexport function stripRecommendationMarker(text) {\n if (typeof text !== 'string') return { text, found: false };\n const match = BRACKETED_SUFFIX.exec(text) ?? BARE_SUFFIX.exec(text);\n if (match === null) return { text, found: false };\n return { text: text.slice(0, match.index).replace(/\\s+$/, ''), found: true };\n}\n\n/**\n * \u628A\u4E00\u4E2A option \u5F52\u4E00\u5316\u6210\u5BA2\u6237\u7AEF\u80FD\u6E32\u67D3\u7684\u5F62\u5F0F\u3002\n * @param {{ label: string, description?: string, recommended?: boolean }} option - \u6A21\u578B\u7ED9\u7684\u9009\u9879\u3002\n * @returns {{ originalLabel: string, label: string, description?: string, recommended: boolean }}\n * \u4EA4\u7ED9\u754C\u9762\u65F6\u7528 label/description\uFF1B\u56DE\u4F20\u7B54\u6848\u65F6\u7528 originalLabel \u8FD8\u539F\u6A21\u578B\u539F\u672C\u5199\u7684 label\u3002\n */\nexport function normalizeOption(option) {\n const label = stripRecommendationMarker(option.label);\n const description = option.description === undefined ? undefined : stripRecommendationMarker(option.description);\n const recommended = option.recommended === true || label.found || (description !== undefined && description.found);\n const cleanLabel = label.text;\n return {\n originalLabel: option.label,\n label: recommended ? `${cleanLabel}${RECOMMENDED_LABEL_SUFFIX}` : cleanLabel,\n description: description === undefined ? undefined : description.text,\n recommended,\n };\n}\n"],
5
- "mappings": "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",
3
+ "sources": ["../src/index.js", "../src/recommendation.js", "../src/form-model.js"],
4
+ "sourcesContent": ["import { defineTool } from \"@deepseek-ai/dsh-tools\";\nimport { validateRound } from \"./form-model.js\";\n\n/**\n * @lynn123411/dsh-ask-user-grilling \u2014 grilling \u8F6E\u7684\u9010\u9898\u76D8\u95EE\u5DE5\u5177\u3002\n *\n * grilling_form \u4E0E\u539F\u751F ask_user_question\uFF08@deepseek-ai/dsh-tool-ask-user\uFF09\u662F\u4E24\u4EF6\u4E8B\uFF0C\n * \u5206\u5DE5\u5199\u5728\u81EA\u6211\u4ECB\u7ECD\u91CC\uFF1A\u539F\u751F\u90A3\u6761\u8D70 ctx.userQuestions seam\u3001\u5728\u8F93\u5165\u6846\u4E0A\u65B9\u63A5\u7BA1\u4F5C\u7B54\u9762\u677F\u3001\n * \u963B\u585E\u7B49\u7B54\u6848\uFF0C\u9002\u5408\u4E00\u6B21\u7B80\u5355\u786E\u8BA4\uFF1B\u8FD9\u4E00\u6761\u4E0D\u51FA\u6587\u672C\u3001\u4E0D\u963B\u585E\uFF0C\u628A\u6574\u8F6E\u95EE\u9898\u4F5C\u4E3A\u4E00\u5F20**\u53EF\u4EA4\u4E92\u8868\u5355**\n * \u6295\u5728\u5BF9\u8BDD\u8BB0\u5F55\u7684\u90A3\u6761\u5DE5\u5177\u884C\u91CC\uFF0C\u81EA\u5DF1\u7ACB\u523B\u7ED3\u675F\u672C\u8F6E\u3002\n *\n * \u4E00\u6761\u8C03\u7528\u53EA\u505A\u4E09\u4EF6\u4E8B\uFF1A\n * - \u6821\u9A8C\u5165\u53C2\uFF08\u89C1 validateRound\uFF09\uFF1A\u91CD\u590D id\u3001\u7A7A\u9898\u5E72\u3001\u7A7A label\u3001\u540C\u9898\u5F52\u4E00\u5316\u540E\u91CD\u540D label\uFF0C\n * \u4EE5\u53CA\u5C55\u5F00\u6B63\u6587\u91CC\u628A\u9009\u9879\u53C8\u5217\u4E00\u904D\uFF08\u90A3\u4F1A\u8BA9\u9875\u9762\u4E0A\u51FA\u73B0\u4E24\u904D\u9009\u9879\uFF0C\u4E00\u904D\u5E26\u9009\u62E9\u5668\u4E00\u904D\u4E0D\u5E26\uFF09\n * \u547D\u4E2D\u5373\u8FD4\u56DE\u53EF\u4FEE\u590D\u7684\u62D2\u7EDD\uFF0C\u4E14**\u4E0D\u6807\u8BB0\u7ED3\u675F**\uFF0C\u8BA9\u6A21\u578B\u6539\u5B8C\u91CD\u8C03\uFF1B\n * - \u901A\u8FC7\u6821\u9A8C\u5C31\u6807\u8BB0\u672C\u8F6E\u7ED3\u675F\uFF08exec.concludeTurn()\uFF09\u2014\u2014\u672C\u8F6E\u5230\u6B64\u4E3A\u6B62\uFF0C\u4E0D\u518D\u7B49\u7B54\u6848\uFF1B\n * - \u8FD4\u56DE\u4E00\u6761 pending \u58F0\u660E\uFF1A\u8868\u5355\u5DF2\u5C55\u793A\u3001\u7528\u6237\u4F1A\u5728\u4ED6\u81EA\u5DF1\u7684\u8F93\u5165\u6846\u91CC\u4F5C\u7B54\u3001\u90A3\u6761\u53D1\u8A00\u968F\u540E\u4F5C\u4E3A\n * \u4E0B\u4E00\u6761\u7528\u6237\u6D88\u606F\u5230\u8FBE\uFF0C\u522B\u628A\u5B83\u5F53\u6210\u5DF2\u56DE\u7B54\u3001\u4E5F\u522B\u518D\u5199\u4E00\u904D\u9898\u76EE\u3002\n *\n * \u8868\u5355\u672C\u8EAB\u7531\u6D4F\u89C8\u5668\u534A\u8FB9\u753B\uFF08\u5BF9\u8BDD\u8BB0\u5F55\u91CC\u6309 wire \u5DE5\u5177\u540D\u6CE8\u518C\u7684 tool.call.toolview \u884C\uFF09\uFF0C\n * \u4F5C\u7B54\u7531\u7528\u6237\u70B9\u8868\u5355\u5E95\u90E8\u7684\u300C\u5BFC\u5165\u8F93\u5165\u6846\u300D\u5199\u8FDB\u539F\u751F\u8349\u7A3F\u3001\u518D\u7528\u539F\u751F\u53D1\u9001\u952E\u53D1\u51FA\u3002\u63D2\u4EF6\u4E0D\u78B0\u8F93\u5165\u6846\u3002\n *\n * \u4E24\u6761\u884C\u6302\u540C\u4E00\u4E2A\u6A21\u5757\u3002preset \u5DE5\u5177\u884C\u6CE8\u518C\u5DE5\u5177\uFF08\u9ED8\u8BA4\uFF09\u3002profile bundle \u884C\u7531\u672C\u5305\u7684\n * `cordis.patch.yml` \u63D2\u5165\uFF0C\u53EA\u4E3A\u8BA9 Web client \u670D\u52A1 `lib/client.js`\u2014\u2014\u6D4F\u89C8\u5668 bundle \u53EA\u5BF9\n * enabled Loader entry \u670D\u52A1\uFF0C\u800C preset \u884C\u662F\u76F4\u63A5\u6302\u7684\u5B50\u6811\uFF1B\u90A3\u6761\u884C\u4F20 `config.carrier: true`\uFF0C\n * \u4EC0\u4E48\u90FD\u4E0D\u6CE8\u518C\uFF0C\u5426\u5219\u5DE5\u5177\u4F1A\u4EE5 root layer \u8EAB\u4EFD\u5BF9\u6BCF\u4E00\u4E2A preset \u7684 agent \u53EF\u89C1\u3002\n */\nconst name = \"tool-grilling-form\";\nconst inject = [\"tools\"];\n\n/** \u8F7D\u4F53\u884C\u8BA4\u8FD9\u4E00\u4E2A\u952E\uFF0C\u8BA4\u5230\u5C31\u6574\u884C\u4EC0\u4E48\u90FD\u4E0D\u6CE8\u518C\uFF08\u6D4F\u89C8\u5668\u534A\u8FB9\u7684 servable entry\uFF0C\u89C1\u6587\u4EF6\u5934\uFF09\u3002 */\nconst CONFIG_KEY = \"carrier\";\n\n/** \u8868\u5355\u843D\u5E93\u4E4B\u540E\u7ED9\u6A21\u578B\u7684\u90A3\u6BB5\u8BDD\uFF1A\u5B83\u5FC5\u987B\u77E5\u9053\u672C\u8F6E\u5DF2\u7ECF\u7ED3\u675F\u3001\u7B54\u6848\u4F1A\u665A\u5230\u3001\u522B\u5F53\u6210\u5DF2\u56DE\u7B54\u3002 */\nconst PENDING_MESSAGE = \"The form with this round's questions is now shown in the conversation and this turn has ended. The user has not answered yet: they answer in their own composer, and their answers arrive later as their next user message, one line per answered question (\\\"<title>: <chosen options / typed text>\\\"). Do not wait for it, do not treat this as an answer, and do not write the questions in prose as well.\";\n\n/**\n * \u8BFB\u8FD9\u4E00\u884C\u7684 config\u3002\u672A\u77E5\u952E\u4E0E\u9519\u7C7B\u578B\u5F53\u573A\u629B\uFF1A\u62FC\u9519\u7684\u8F7D\u4F53\u884C\u4F1A\u9759\u9ED8\u591A\u6CE8\u518C\u4E00\u4E2A\u5168\u5C40\u5DE5\u5177\uFF0C\n * \u90A3\u6BD4\u542F\u52A8\u5931\u8D25\u96BE\u67E5\u5F97\u591A\u3002\n * @param {unknown} config - loader \u4EA4\u7ED9\u63D2\u4EF6\u884C\u7684 config\u3002\n * @returns {{ carrier: boolean }} \u5F52\u4E00\u5316\u540E\u7684\u914D\u7F6E\u3002\n */\nfunction readConfig(config) {\n if (config === undefined || config === null) return { carrier: false };\n if (typeof config !== \"object\" || Array.isArray(config)) {\n throw new TypeError(`${name}: config must be an object with an optional boolean \"${CONFIG_KEY}\"`);\n }\n const unknown = Object.keys(config).filter((key) => key !== CONFIG_KEY);\n if (unknown.length > 0) {\n throw new TypeError(`${name}: unknown config key(s) ${unknown.map((key) => JSON.stringify(key)).join(\", \")}; the only accepted key is \"${CONFIG_KEY}\"`);\n }\n if (config[CONFIG_KEY] !== undefined && typeof config[CONFIG_KEY] !== \"boolean\") {\n throw new TypeError(`${name}: config.${CONFIG_KEY} must be a boolean`);\n }\n return { carrier: config[CONFIG_KEY] === true };\n}\n\nfunction apply(ctx, config) {\n if (readConfig(config).carrier) return;\n ctx.tools.register(defineTool({\n name: \"grilling_form\",\n description: \"Put this round's grilling questions to the user as a form in the conversation, then end your turn. The user answers it in their own composer and their answers arrive as their next user message. Use it only for a round of per-question grilling, and make it the only and final tool call of the round: it ends the turn, so anything else in the same batch keeps the turn open.\",\n parameters: {\n questions: {\n type: \"array\",\n required: true,\n description: \"The round's questions, in the order the user should read them.\",\n items: {\n type: \"object\",\n additionalProperties: true,\n properties: {\n id: {\n type: \"string\",\n required: true,\n description: \"Stable id for this question; the form keys the user's answer by it.\",\n },\n question: {\n type: \"string\",\n required: true,\n description: \"The specific question to ask the user. Keep it to the one line that asks; put any longer body in detail.\",\n },\n header: {\n type: \"string\",\n description: \"Optional short heading for the question, such as \\\"Confirm\\\" or \\\"Choose Mode\\\".\",\n },\n number: {\n type: \"string\",\n description: \"Optional question number, e.g. \\\"Q2\\\". The form renders it before the header as \\\"Q2 \u00B7 <header>\\\".\",\n },\n detail: {\n type: \"string\",\n description: \"Optional supporting body for this question, rendered as markdown under the question. Use it only for what all the options share; a line that begins like one of the option labels is rejected, because the options would then be printed twice (once as prose, once as choices).\",\n },\n options: {\n type: \"array\",\n description: \"Optional choices the form shows for this question. If you recommend one, put it first and either append \\\"\uFF08\u63A8\u8350\uFF09\\\" to its label or set its recommended flag.\",\n items: {\n type: \"object\",\n additionalProperties: true,\n properties: {\n label: {\n type: \"string\",\n required: true,\n description: \"Short user-facing option label.\",\n },\n description: {\n type: \"string\",\n description: \"One sentence explaining the tradeoff or impact.\",\n },\n recommended: {\n type: \"boolean\",\n description: \"Set true on the option you recommend (alternative to appending \\\"\uFF08\u63A8\u8350\uFF09\\\" to its label). A recommended option is checked by default in the form, so mark only what you would pick yourself.\",\n },\n },\n },\n },\n },\n },\n },\n },\n output: {\n schema: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n rejected: {\n type: \"boolean\",\n description: \"True when input validation failed and no form was shown.\",\n },\n violations: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Validation violations when rejected.\",\n },\n error: {\n type: \"string\",\n description: \"Human-readable error when rejected.\",\n },\n pending: {\n type: \"boolean\",\n description: \"True when the form is displayed and this turn has ended; the user has not answered yet.\",\n },\n message: {\n type: \"string\",\n description: \"What this pending round means for you.\",\n },\n },\n },\n render: (_args, value) => [{ type: \"text\", text: JSON.stringify(value) }],\n },\n async execute(args, exec) {\n // 1. input validation: only what the form cannot represent at all. A rejected round is\n // NOT concluded \u2014 the model must be able to fix it and call again this same turn.\n const violations = validateRound(args.questions);\n if (violations.length > 0) {\n return {\n rejected: true,\n violations,\n error: `${violations.length} violation(s) in this round: ids must be unique, question text must not be blank, and the options of one question must not share a label. Fix them and call this tool again.`,\n };\n }\n\n // 2. the form is now on its way into the transcript, and this tool has nothing left to\n // wait for: mark the turn concluded. The end point is \"after this batch's results are\n // recorded and the next-step inbox is empty\", which is why the description and the\n // skill body both demand that this be the only and final call of the round.\n exec.concludeTurn();\n return { pending: true, message: PENDING_MESSAGE };\n },\n }));\n}\n\nexport { apply, inject, name };\n", "// \u63A8\u8350\u6807\u8BB0\u7684\u5F52\u4E00\u5316\u3002\n//\n// \u6A21\u578B\u5199\u63A8\u8350\u7684\u65B9\u5F0F\u4E94\u82B1\u516B\u95E8\uFF1Adescription \u672B\u5C3E\u7684\u300C\uFF1B\u63A8\u8350\u300D\u3001label \u91CC\u4E0D\u5E26\u62EC\u53F7\u7684\u5199\u6CD5\u3001\u65B9\u62EC\u53F7\u3001\n// \u6216\u5E72\u8106\u7ED9\u4E00\u4E2A recommended: true \u5B57\u6BB5\u3002\u754C\u9762\u53EA\u8BA4\u4E00\u79CD\u5F62\u5F0F\u2014\u2014label \u672B\u5C3E\u7684\u300C\uFF08\u63A8\u8350\uFF09\u300D\u540E\u7F00\uFF0C\n// \u4E8E\u662F\u5FBD\u6807\u4E0D\u6E32\u67D3\u3001\u6807\u8BB0\u539F\u6837\u663E\u793A\u6210\u6587\u5B57\u3002\n//\n// \u8FD9\u91CC\u628A\u6807\u8BB0\u7EDF\u4E00\u642C\u5230 label \u672B\u5C3E\u7684\u300C\uFF08\u63A8\u8350\uFF09\u300D\uFF0C\u754C\u9762\u5C31\u80FD\u4E00\u6B21\u8BA4\u51FA\u6765\u3002\u663E\u5F0F\u5B57\u6BB5 recommended\n// \u662F\u53E6\u4E00\u6761\u66F4\u660E\u786E\u7684\u4FE1\u53F7\uFF0C\u4F18\u5148\u4E8E\u6587\u5B57\u3002\n//\n// \u5F52\u4E00\u5316\u540E\u7684 label \u540C\u65F6\u662F**\u9009\u9879\u7684\u8EAB\u4EFD**\uFF1A\u5BBF\u4E3B\u6309\u5B83\u67E5\u540C\u9898\u5185\u91CD\u540D\u3001\u754C\u9762\u6309\u5B83\u5B58\u52FE\u9009\u3002\u6240\u4EE5\u9664\u4E86\n// \u8FD9\u4E2A\u5E26\u540E\u7F00\u7684 label\uFF0C\u8FD8\u7ED9\u51FA cleanLabel\uFF08\u4E0D\u542B\u540E\u7F00\u7684\u5C55\u793A\u6587\u5B57\uFF09\u2014\u2014\u5FBD\u6807\u7531\u754C\u9762\u81EA\u5DF1\u753B\uFF0C\u5BFC\u5165\n// \u5230\u8F93\u5165\u6846\u7684\u4F5C\u7B54\u4E5F\u7528 cleanLabel\uFF0C\u6A21\u578B\u5199\u7684\u63A8\u8350\u6807\u8BB0\u4E0D\u4F1A\u8DDF\u7740\u7528\u6237\u7684\u6D88\u606F\u53D1\u56DE\u53BB\u3002\n\nconst MARKER = 'recommended|\u63A8\u8350';\n// \u534A\u89D2/\u5168\u89D2\u5706\u62EC\u53F7\u4E0E\u65B9\u62EC\u53F7/\u65B9\u5934\u62EC\u53F7\nconst BRACKETED_MARKER = `[\uFF08(\u3010\\\\[]\\\\s*(?:${MARKER})\\\\s*[\uFF09)\u3011\\\\]]`;\n// \u65E0\u62EC\u53F7\u5199\u6CD5\u5FC5\u987B\u7531\u5206\u9694\u7B26\u5F15\u51FA\uFF0C\u5426\u5219\u300C\u67E5\u770B\u63A8\u8350\u300D\u8FD9\u79CD\u6B63\u5E38\u6587\u5B57\u4F1A\u88AB\u8BEF\u5224\nconst SEPARATOR = '\uFF1B;\uFF0C,\u3001:\uFF1A\\\\-\u2014';\nconst TRAILING_PUNCTUATION = '\u3002.\uFF01!\uFF1F?\uFF1B;\uFF0C,\u3001~\uFF5E\\\\s';\n\nconst BRACKETED_SUFFIX = new RegExp(`[\\\\s]*[${SEPARATOR}]?\\\\s*${BRACKETED_MARKER}[${TRAILING_PUNCTUATION}]*$`, 'i');\nconst BARE_SUFFIX = new RegExp(`[\\\\s]*[${SEPARATOR}]\\\\s*(?:${MARKER})[${TRAILING_PUNCTUATION}]*$`, 'i');\n\n/** \u754C\u9762\u4E0A\u63A8\u8350\u9009\u9879\u7684\u6807\u8BB0\u540E\u7F00\uFF1B\u4E5F\u662F\u5F52\u4E00\u5316\u540E label \u7684\u6536\u5C3E\u3002 */\nexport const RECOMMENDED_LABEL_SUFFIX = '\uFF08\u63A8\u8350\uFF09';\n\n/**\n * \u53BB\u6389\u6587\u672C\u672B\u5C3E\u7684\u63A8\u8350\u6807\u8BB0\u3002\n * @param {string} text - label \u6216 description \u539F\u6587\u3002\n * @returns {{ text: string, found: boolean }} \u53BB\u6389\u6807\u8BB0\u540E\u7684\u6587\u672C\uFF0C\u4EE5\u53CA\u662F\u5426\u627E\u5230\u6807\u8BB0\u3002\n */\nexport function stripRecommendationMarker(text) {\n if (typeof text !== 'string') return { text, found: false };\n const match = BRACKETED_SUFFIX.exec(text) ?? BARE_SUFFIX.exec(text);\n if (match === null) return { text, found: false };\n return { text: text.slice(0, match.index).replace(/\\s+$/, ''), found: true };\n}\n\n/**\n * \u628A\u4E00\u4E2A option \u5F52\u4E00\u5316\u6210\u754C\u9762\u80FD\u6E32\u67D3\u7684\u5F62\u5F0F\u3002\n * @param {{ label: string, description?: string, recommended?: boolean }} option - \u6A21\u578B\u7ED9\u7684\u9009\u9879\u3002\n * @returns {{ originalLabel: string, label: string, cleanLabel: string, description?: string, recommended: boolean }}\n * label \u662F\u9009\u9879\u8EAB\u4EFD\uFF08\u5E26\u300C\uFF08\u63A8\u8350\uFF09\u300D\u540E\u7F00\uFF0C\u5BBF\u4E3B\u6309\u5B83\u67E5\u91CD\u3001\u754C\u9762\u6309\u5B83\u5B58\u52FE\u9009\uFF09\uFF1BcleanLabel \u662F\u7ED9\n * \u7528\u6237\u770B\u7684\u6587\u5B57\uFF08recommended \u4E3A true \u65F6\u7531\u754C\u9762\u53E6\u753B\u5FBD\u6807\uFF09\uFF1BoriginalLabel \u662F\u6A21\u578B\u539F\u672C\u5199\u7684 label\u3002\n */\nexport function normalizeOption(option) {\n const label = stripRecommendationMarker(option.label);\n const description = option.description === undefined ? undefined : stripRecommendationMarker(option.description);\n const recommended = option.recommended === true || label.found || (description !== undefined && description.found);\n const cleanLabel = label.text;\n return {\n originalLabel: option.label,\n label: recommended ? `${cleanLabel}${RECOMMENDED_LABEL_SUFFIX}` : cleanLabel,\n cleanLabel,\n description: description === undefined ? undefined : description.text,\n recommended,\n };\n}\n", "// grilling_form \u7684\u5171\u7528\u7EAF\u6A21\u578B\uFF1A\u5BBF\u4E3B\u534A\u8FB9\u7684\u5165\u53C2\u6821\u9A8C\u3001\u754C\u9762\u534A\u8FB9\u7684\u53D6\u6570\u4E0E\u4F5C\u7B54\u5BFC\u5165\uFF0C\u5168\u5728\u8FD9\u4E00\u4E2A\u6A21\u5757\u91CC\u3002\n//\n// \u4E24\u7AEF\u5FC5\u987B\u4E00\u81F4\u7684\u4E24\u4EF6\u4E8B\u90FD\u53EA\u6709\u8FD9\u4E00\u4EFD\u5B9E\u73B0\uFF1A\u9898\u53F7\u5E76\u5165\u6807\u9898\u7684\u62FC\u6CD5\uFF08\u5BBF\u4E3B\u4E0D\u62FC\u3001\u754C\u9762\u62FC\uFF0C\u4F46\u6A21\u578B\u81EA\u5DF1\n// \u5BA3\u5E03\u7684 Q \u7F16\u53F7\u8981\u5BF9\u5F97\u4E0A\uFF09\uFF0C\u4EE5\u53CA\u5F52\u4E00\u5316\u540E\u7684 option label \u8BA4\u9009\u9879\uFF08\u5BBF\u4E3B\u6309\u5B83\u67E5\u91CD\u3001\u754C\u9762\u6309\u5B83\u5B58\u52FE\u9009\u3001\n// \u751F\u6210\u5BFC\u5165\u6587\u672C\u65F6\u518D\u6362\u56DE\u6A21\u578B\u539F\u672C\u5199\u7684 label\uFF09\u3002\u5404\u5199\u4E00\u4EFD\u5FC5\u7136\u6F02\u3002\n//\n// \u7EAF\u51FD\u6570\uFF1A\u4E0D\u78B0 React\u3001\u4E0D\u8BFB\u65F6\u949F\u3001\u4E0D\u8BFB\u4F1A\u8BDD\u3002localStorage \u7531\u8C03\u7528\u65B9\u4F20\u8FDB\u6765\uFF08\u89C1\u6587\u4EF6\u672B\u5C3E\u90A3\u7EC4\u4EE5 storage \u4E3A\u53C2\u6570\u7684\u51FD\u6570\uFF09\u3002\n\nimport { normalizeOption } from './recommendation.js';\n\n/** \u5BFC\u5165\u5757\u7684\u8D77\u6B62\u884C\uFF1A\u91CD\u590D\u5BFC\u5165\u65F6\u9760\u5B83\u8BA4\u51FA\u4E0A\u4E00\u6B21\u5BFC\u5165\u7684\u90A3\u4E00\u6BB5\uFF0C\u539F\u5730\u66FF\u6362\u3002 */\nexport const IMPORT_BLOCK_START = '\u3010\u76D8\u95EE\u4F5C\u7B54\u3011';\nexport const IMPORT_BLOCK_END = '\u3010/\u76D8\u95EE\u4F5C\u7B54\u3011';\n\nfunction isRecord(value) {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction parseJson(text) {\n if (typeof text !== 'string' || text === '') return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nfunction stringList(value) {\n return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];\n}\n\n/**\n * \u53BB\u6389\u9996\u5C3E\u7A7A\u767D\u540E\u662F\u5426\u4E3A\u7A7A\u3002\u7A7A\u4E32\u4E0E\u7EAF\u7A7A\u767D\u90FD\u4E0D\u503C\u5F97\u9001\u8FDB\u8868\u5355\u3002\n * @param {unknown} value - \u5F85\u5224\u5B9A\u7684\u503C\u3002\n * @returns {boolean} \u975E\u5B57\u7B26\u4E32\u6216\u53BB\u7A7A\u767D\u540E\u4E3A\u7A7A\u65F6\u4E3A true\u3002\n */\nexport function isBlank(value) {\n return typeof value !== 'string' || value.trim() === '';\n}\n\n/**\n * \u628A\u9898\u53F7\u5E76\u8FDB\u6807\u9898\uFF1A\u4E24\u8005\u90FD\u7ED9\u5199\u4F5C `<number> \u00B7 <header>`\uFF0C\u53EA\u7ED9\u4E00\u4E2A\u5C31\u662F\u90A3\u4E00\u4E2A\uFF1B\n * \u6807\u9898\u5DF2\u7ECF\u4EE5\u8BE5\u9898\u53F7\u5F00\u5934\u65F6\u539F\u6837\u7528\u5B83\uFF08\u6A21\u578B\u5E38\u81EA\u5DF1\u5199\u6210 \"Q2 \u00B7 Deadline\"\uFF09\u3002\n * @param {string} [number] - \u9898\u53F7\u3002\n * @param {string} [header] - \u6807\u9898\u3002\n * @returns {string} \u754C\u9762\u6807\u9898\uFF1B\u4E24\u8005\u90FD\u7F3A\u65F6\u4E3A\u7A7A\u4E32\uFF08\u8C03\u7528\u65B9\u636E\u6B64\u9000\u56DE\u9898\u5E72\uFF09\u3002\n */\nexport function mergeNumberIntoHeader(number, header) {\n const num = isBlank(number) ? undefined : number;\n const head = isBlank(header) ? undefined : header;\n if (num === undefined) return head ?? '';\n if (head === undefined) return num;\n return head.startsWith(num) ? head : `${num} \u00B7 ${head}`;\n}\n\n/**\n * \u4E00\u9053\u9898\u5728\u754C\u9762\u4E0E\u5BFC\u5165\u6587\u672C\u91CC\u7684\u6807\u9898\uFF1A\u9898\u53F7\u5E76\u5165\u540E\u7684\u6807\u9898\u4F18\u5148\uFF0C\u6CA1\u6709\u5C31\u7528\u9898\u5E72\uFF0C\u518D\u6CA1\u6709\u5C31\u7528 id\u3002\n * @param {{ id: string, header: string, question: string }} question - \u9898\u76EE\u6A21\u578B\u3002\n * @returns {string} \u6807\u9898\u3002\n */\nexport function titleOf(question) {\n if (!isBlank(question.header)) return question.header;\n if (!isBlank(question.question)) return question.question;\n return question.id;\n}\n\n/** \u5C55\u5F00\u6B63\u6587\u91CC\u4E00\u884C\u7684\u6392\u7248\u7B26\u53F7\uFF1A\u5217\u8868\u7B26\u53F7\uFF08\u542B\u300C1\uFF09\u300D\u8FD9\u79CD\u5168\u89D2\u5199\u6CD5\uFF09\u3001\u5C0F\u6807\u9898\u3001\u5F15\u7528\u5757\uFF0C\u53BB\u6389\u4E4B\u540E\u624D\u770B\u5F97\u51FA\u5B83\u662F\u4E0D\u662F\u4ECE\u9009\u9879\u540D\u5F00\u7684\u5934\u3002 */\nconst LINE_MARKERS = /^\\s*(?:[-*+]\\s+|\\d+[.)\u3001\uFF09]\\s*|#{1,6}\\s+|>\\s*)+/;\n/** \u884C\u5185\u5F3A\u8C03\u7B26\u53F7\uFF1A`**\u7532**`\u3001`*\u7532*` \u4E0E `\u7532` \u662F\u540C\u4E00\u4E2A\u9009\u9879\u540D\u3002 */\nconst INLINE_MARKERS = /[*_`]/g;\n/** \u9009\u9879\u6807\u7B7E\u91CC\u7B2C\u4E00\u4E2A\u5206\u9694\u7B26\u4E4B\u524D\u7684\u90A3\u4E00\u6BB5\uFF0C\u5C31\u662F\u5B83\u5728\u5C55\u5F00\u6B63\u6587\u91CC\u4F1A\u4EE5\u4EC0\u4E48\u6837\u5B50\u5F00\u5934\u3002 */\nconst LABEL_ALIAS_END = /[\uFF1A:\u00B7\u3001\uFF0C,\u3002;\uFF1B!\uFF01?\uFF1F(\uFF08)\uFF09\\-\u2013\u2014\\s]/;\n/** \u522B\u540D\uFF08\u5176\u540E\u53EF\u4EE5\u6709\u4E00\u4E2A\u7A7A\u683C\uFF09\u8DDF\u8FD9\u4E9B\u7B26\u53F7\u624D\u7B97\u540C\u4E00\u4E2A\u6761\u76EE\u3002\u53EA\u8BA4\u300C\u540D\uFF1A\u89E3\u91CA\u300D\u90A3\u4E00\u7C7B\u5206\u9694\u7B26\uFF1A\n * \u987F\u53F7\u4E0E\u9017\u53F7\u4E0D\u7B97\uFF0C\u5426\u5219\u6B63\u6587\u91CC\u300C\u7532\u3001\u4E59\u4E24\u79CD\u505A\u6CD5\u5404\u6709\u4EE3\u4EF7\u300D\u8FD9\u79CD\u6B63\u7ECF\u7684\u5171\u7528\u80CC\u666F\u4F1A\u88AB\u8BEF\u62D2\u3002 */\nconst ALIAS_FOLLOW_CHAR = /[\uFF1A:\u00B7\uFF09)\\-\u2013\u2014>]/;\n\nfunction shorten(text) {\n return text.length <= 60 ? text : `${text.slice(0, 60)}\u2026`;\n}\n\n/**\n * \u4E00\u4E2A\u9009\u9879\u6807\u7B7E\u7684\u522B\u540D\uFF1A\u5230\u7B2C\u4E00\u4E2A\u5206\u9694\u7B26\u4E3A\u6B62\u7684\u90A3\u4E00\u6BB5\u3002\u300C\u7532\uFF1A\u8BE6\u60C5\u9875\u91CC\u7684\u914D\u7F6E\u5165\u53E3\u300D\u2192\u300C\u7532\u300D\uFF0C\n * \u300C\u4FDD\u7559\u73B0\u72B6\u300D\u2192\u300C\u4FDD\u7559\u73B0\u72B6\u300D\u3002\u6A21\u578B\u5728\u5C55\u5F00\u6B63\u6587\u91CC\u91CD\u8FF0\u9009\u9879\u65F6\uFF0C\u5F00\u5934\u5199\u7684\u901A\u5E38\u5C31\u662F\u8FD9\u4E2A\u522B\u540D\u3002\n * @param {string} label - \u5F52\u4E00\u5316\u540E\u7684\u5C55\u793A\u6587\u5B57\uFF08\u4E0D\u542B\u300C\uFF08\u63A8\u8350\uFF09\u300D\u540E\u7F00\uFF09\u3002\n * @returns {string} \u522B\u540D\uFF1B\u7A7A\u6807\u7B7E\u65F6\u4E3A\u7A7A\u4E32\uFF08\u8C03\u7528\u65B9\u8DF3\u8FC7\uFF09\u3002\n */\nfunction labelAlias(label) {\n const cut = label.search(LABEL_ALIAS_END);\n return cut === -1 ? label : label.slice(0, cut);\n}\n\n/**\n * \u5C55\u5F00\u6B63\u6587\u91CC\u6709\u6CA1\u6709\u628A\u67D0\u4E2A\u9009\u9879\u53C8\u5217\u4E86\u4E00\u904D\u3002\u8BA4\u7684\u662F**\u884C\u9996**\uFF1A\u53BB\u6389\u5217\u8868\u7B26\u53F7\u4E0E\u5F3A\u8C03\u7B26\u53F7\u540E\uFF0C\n * \u4E00\u884C\u4EE5\u67D0\u4E2A\u9009\u9879\u7684\u522B\u540D\u5F00\u5934\u3001\u540E\u9762\uFF08\u53EF\u7A7A\u4E00\u683C\uFF09\u7D27\u8DDF\u5206\u9694\u7B26\u6216\u76F4\u63A5\u7ED3\u675F\uFF0C\u5C31\u662F\u540C\u4E00\u4EF6\u4E8B\u5199\u4E86\u7B2C\u4E8C\u904D\u3002\n *\n * \u53EA\u8BA4\u884C\u9996\u3001\u4E14\u8981\u6C42\u522B\u540D\u540E\u9762\u5C31\u662F\u5206\u9694\u7B26\uFF0C\u662F\u4E3A\u4E86\u628A\u8BEF\u62D2\u9762\u538B\u5230\u6700\u5C0F\uFF1A\u6B63\u6587\u91CC\u987A\u53E3\u63D0\u5230\u67D0\u4E2A\u9009\u9879\u540D\n * \uFF08\u300C\u9009\u7532\uFF1A\u2026\u300D\uFF09\u4E0D\u7B97\uFF0C\u53EA\u6709\u300C\u4E00\u6761\u4EE5\u9009\u9879\u540D\u5F00\u5934\u7684\u6761\u76EE\u300D\u624D\u7B97\u3002\u4EE3\u4EF7\u662F\u6F0F\u653E\u4E0E\u8BEF\u62D2\u90FD\u8FD8\u53EF\u80FD\u53D1\u751F\uFF0C\n * \u6240\u4EE5\u6280\u80FD\u6B63\u6587\u90A3\u534A\u8FB9\uFF08\u4E09\u4EFD preset \u91CC grilling \u6280\u80FD\u7684\u5B57\u6BB5\u6620\u5C04\u6BB5\uFF09\u662F\u7B2C\u4E00\u9053\u95F8\u3002\n * @param {string} detail - \u5C55\u5F00\u6B63\u6587\uFF08markdown\uFF09\u3002\n * @param {string[]} aliases - \u8BE5\u9898\u5404\u9009\u9879\u7684\u522B\u540D\u3002\n * @returns {string|undefined} \u547D\u4E2D\u7684\u90A3\u4E00\u884C\uFF1B\u6CA1\u547D\u4E2D\u65F6 undefined\u3002\n */\nfunction detailRestatesOption(detail, aliases) {\n if (isBlank(detail)) return undefined;\n for (const raw of detail.split('\\n')) {\n const line = raw.replace(LINE_MARKERS, '').replace(INLINE_MARKERS, '').trim();\n if (line === '') continue;\n for (const alias of aliases) {\n if (alias === '' || !line.startsWith(alias)) continue;\n const rest = line.slice(alias.length).trimStart();\n if (rest === '' || ALIAS_FOLLOW_CHAR.test(rest[0])) return line;\n }\n }\n return undefined;\n}\n\n/**\n * \u8FD9\u4E00\u8F6E\u7684\u5165\u53C2\u80FD\u4E0D\u80FD\u753B\u6210\u8868\u5355\u3002\u9898\u5E72\u4E0E\u9009\u9879\u7684\u5206\u5DE5\u6574\u4F53\u4E0A\u662F\u5F15\u5BFC\u2014\u2014\u5B50\u4E32\u5339\u914D\u4F1A\u8BEF\u62D2\u5408\u6CD5\u9898\u5E72\uFF0C\n * \u574F\u9898\u5E72\u597D\u8FC7\u5047\u62D2\u7EDD\u2014\u2014\u53EA\u6709\u4E00\u5904\u4F8B\u5916\uFF1A\u5C55\u5F00\u6B63\u6587\u91CC\u628A\u9009\u9879\u53C8\u5217\u4E00\u904D\uFF0C\u90A3\u4F1A\u8BA9\u540C\u4E00\u6279\u9009\u9879\u5728\u9875\u9762\u4E0A\n * \u51FA\u73B0\u4E24\u904D\uFF08\u4E00\u904D\u5E26\u9009\u62E9\u5668\u3001\u4E00\u904D\u4E0D\u5E26\uFF09\uFF0C\u89C1 detailRestatesOption\u3002\n * @param {object[]} questions - \u6A21\u578B\u7ED9\u7684 questions\u3002\n * @returns {string[]} \u9010\u6761\u8FDD\u89C4\u8BF4\u660E\uFF1B\u7A7A\u6570\u7EC4\u8868\u793A\u8FD9\u4E00\u8F6E\u53EF\u4EE5\u6295\u51FA\u53BB\u3002\n */\nexport function validateRound(questions) {\n const violations = [];\n const seenIds = new Set();\n questions.forEach((question, questionIndex) => {\n const where = `Question ${questionIndex + 1} (id ${JSON.stringify(question.id)})`;\n if (typeof question.id === 'string') {\n if (seenIds.has(question.id)) {\n violations.push(`${where} repeats an id already used in this round; the form keys each answer by id, so ids must be unique`);\n }\n seenIds.add(question.id);\n }\n if (isBlank(question.question)) {\n violations.push(`${where} has an empty question text; write the question itself in question and its body in detail`);\n }\n const seenLabels = new Set();\n const aliases = [];\n (question.options ?? []).forEach((option, optionIndex) => {\n const normalized = normalizeOption(option);\n if (isBlank(normalized.label)) {\n violations.push(`${where}, option ${optionIndex + 1} has an empty label; every option needs a label the user can read and pick`);\n return;\n }\n if (seenLabels.has(normalized.label)) {\n violations.push(`${where} has two options that render with the same label ${JSON.stringify(normalized.label)}; the form identifies a choice by its label, so make them differ`);\n return;\n }\n seenLabels.add(normalized.label);\n aliases.push(labelAlias(normalized.cleanLabel));\n });\n const restated = detailRestatesOption(question.detail, aliases);\n if (restated !== undefined) {\n violations.push(`${where} restates its options in detail: the line ${JSON.stringify(shorten(restated))} begins like one of the option labels, so the form would show those options twice; options go only in options, and detail is for what all of them share`);\n }\n });\n return violations;\n}\n\n/**\n * argsRaw \u2192 \u6A21\u578B\u51FA\u7684\u9898\u3002\u89E3\u6790\u5931\u8D25\u3001\u6CA1\u6709 questions\u3001\u6216 id \u91CD\u590D\uFF08\u754C\u9762\u6309 id \u914D\u5BF9\uFF0C\u91CD\u590D\u5373\u65E0\u4ECE\u914D\u5BF9\uFF09\u65F6\u8FD4\u56DE\u7A7A\u6570\u7EC4\u3002\n * @param {unknown} argsRaw - \u8C03\u7528\u7684\u539F\u59CB\u53C2\u6570\u6587\u672C\u3002\n * @returns {object[]} \u9898\u76EE\u6A21\u578B\u3002\n */\nfunction readQuestions(argsRaw) {\n const parsed = parseJson(argsRaw);\n if (!isRecord(parsed) || !Array.isArray(parsed.questions)) return [];\n const questions = [];\n const ids = new Set();\n for (const question of parsed.questions) {\n if (!isRecord(question) || typeof question.id !== 'string' || ids.has(question.id)) return [];\n ids.add(question.id);\n const options = (Array.isArray(question.options) ? question.options : [])\n .filter((option) => isRecord(option))\n .map((option) => {\n const normalized = normalizeOption(option);\n return {\n // \u754C\u9762\u6309 label \u8BA4\u9009\u9879\uFF08\u5BBF\u4E3B\u5DF2\u4FDD\u8BC1\u540C\u9898\u5185\u552F\u4E00\uFF09\uFF0C\u663E\u793A\u7528 cleanLabel\u3001\u5FBD\u6807\u53E6\u753B\uFF0C\n // \u5BFC\u5165\u6587\u672C\u4E5F\u7528 cleanLabel\uFF1A\u6A21\u578B\u5199\u7684\u63A8\u8350\u6807\u8BB0\u4E0D\u4F1A\u8DDF\u7740\u7528\u6237\u7684\u6D88\u606F\u53D1\u56DE\u53BB\u3002\n label: normalized.label,\n displayLabel: normalized.cleanLabel,\n description: normalized.description,\n recommended: normalized.recommended,\n };\n });\n questions.push({\n id: question.id,\n header: mergeNumberIntoHeader(question.number, question.header),\n question: typeof question.question === 'string' ? question.question : '',\n detail: isBlank(question.detail) ? undefined : question.detail,\n options,\n });\n }\n return questions;\n}\n\n/**\n * \u7ED3\u679C\u6587\u672C \u2192 \u8FD9\u4E00\u8F6E\u6295\u51FA\u53BB\u4E4B\u540E\u53D1\u751F\u4E86\u4EC0\u4E48\u3002\n * @param {unknown} resultText - \u5DE5\u5177\u7ED3\u679C\u91CC\u7684\u6587\u672C\u3002\n * @returns {{rejected: boolean, violations: string[], error: string|undefined}|undefined}\n * \u5F62\u72B6\u4E0D\u8BA4\u8BC6\u65F6\u8FD4\u56DE undefined\uFF08\u8C03\u7528\u65B9\u6309\u8C03\u7528\u5931\u8D25\u5904\u7406\uFF09\u3002\n */\nfunction readResult(resultText) {\n const parsed = parseJson(resultText);\n if (!isRecord(parsed)) return undefined;\n if (parsed.rejected === true) {\n return {\n rejected: true,\n violations: stringList(parsed.violations),\n error: typeof parsed.error === 'string' ? parsed.error : undefined,\n };\n }\n if (parsed.pending === true) return { rejected: false, violations: [], error: undefined };\n return undefined;\n}\n\n/**\n * \u8BFB\u51FA\u4E00\u4E2A\u8868\u5355\u884C\u7684\u6A21\u578B\uFF1A\u9898\u9762\u6765\u81EA argsRaw\uFF0C\u72B6\u6001\u6765\u81EA\u7ED3\u679C\u3002\n * @param {{ argsRaw?: unknown, resultText?: unknown, failed?: boolean }} input - argsRaw\uFF1A\u8C03\u7528\u7684\u539F\u59CB\u53C2\u6570\uFF1B\n * resultText\uFF1A\u7ED3\u679C\u6587\u672C\uFF08\u8FD8\u6CA1\u843D\u5E93\u65F6\u4E3A\u7A7A\uFF09\uFF1Bfailed\uFF1A\u8FD9\u6B21\u8C03\u7528\u662F\u5426\u4EE5\u5931\u8D25\u6536\u573A\u3002\n * @returns {{\n * state: 'running'|'form'|'rejected'|'error',\n * questions: object[], violations: string[], error: string|undefined,\n * }} \u8868\u5355\u6A21\u578B\uFF1Bquestions \u4E3A\u7A7A\u8868\u793A\u53C2\u6570\u8FD8\u89E3\u6790\u4E0D\u51FA\u6765\uFF08\u8FD0\u884C\u4E2D\u6216\u4E0D\u662F\u672C\u63D2\u4EF6\u7684\u5F62\u72B6\uFF09\u3002\n */\nexport function readFormModel({ argsRaw, resultText, failed = false }) {\n const questions = readQuestions(argsRaw);\n const result = readResult(resultText);\n let state;\n if (result !== undefined) state = result.rejected ? 'rejected' : 'form';\n else if (failed) state = 'error';\n else if (typeof resultText === 'string' && resultText !== '') state = 'error';\n else state = 'running';\n return {\n state,\n questions,\n violations: result === undefined ? [] : result.violations,\n error: result === undefined ? undefined : result.error,\n };\n}\n\n/**\n * \u5F52\u4E00\u5316 localStorage \u91CC\u8BFB\u56DE\u6765\u7684\u4F5C\u7B54\uFF1A\u53EA\u8BA4 `{[id]: {selected, custom}}` \u8FD9\u4E2A\u5F62\u72B6\u3002\n *\n * **\u5F62\u72B6\u5BF9\u5F97\u4E0A\u7684\u7A7A\u4F5C\u7B54\u4E5F\u7559\u7740**\uFF1A\u4E00\u6761\u300Cselected \u662F\u6570\u7EC4\u3001\u4E14\u4E0E custom \u90FD\u7A7A\u300D\u7684\u8BB0\u5F55\u4E0D\u662F\u810F\u6570\u636E\uFF0C\n * \u800C\u662F\u300C\u7528\u6237\u628A\u8FD9\u9898\u6E05\u7A7A\u4E86\u300D\uFF08\u6700\u5E38\u89C1\u7684\u662F\u53D6\u6D88\u6389\u63A8\u8350\u9879\u7684\u9ED8\u8BA4\u52FE\u9009\uFF09\u3002\u5B83\u4E0E\u300C\u7528\u6237\u6CA1\u52A8\u8FC7\u8FD9\u9898\u300D\u5FC5\u987B\n * \u5206\u5F97\u5F00\uFF0C\u5426\u5219\u9ED8\u8BA4\u52FE\u9009\u4F1A\u5728\u4ED6\u53D6\u6D88\u4E4B\u540E\u53C8\u52FE\u56DE\u6765\u3002\u4E0D\u662F\u8FD9\u4E2A\u5F62\u72B6\u7684\u8BB0\u5F55\uFF08\u6BD4\u5982 selected \u662F\u4E2A\u5B57\u7B26\u4E32\uFF09\n * \u4E00\u5F8B\u4E22\u6389\uFF1A\u90A3\u79CD\u8BB0\u5F55\u4E0D\u662F\u672C\u63D2\u4EF6\u5199\u7684\uFF0C\u8BA4\u6210\u300C\u6E05\u7A7A\u8FC7\u300D\u4F1A\u767D\u767D\u5403\u6389\u9ED8\u8BA4\u52FE\u9009\u3002\n * @param {unknown} value - \u89E3\u6790\u51FA\u6765\u7684\u503C\u3002\n * @returns {object} \u5E72\u51C0\u7684\u4F5C\u7B54\u8868\u3002\n */\nfunction sanitizeAnswers(value) {\n if (!isRecord(value)) return {};\n const answers = {};\n for (const [id, answer] of Object.entries(value)) {\n if (!isRecord(answer)) continue;\n const shaped = Array.isArray(answer.selected) || typeof answer.custom === 'string';\n const selected = stringList(answer.selected);\n const custom = typeof answer.custom === 'string' ? answer.custom : '';\n if (!shaped && selected.length === 0 && custom === '') continue;\n answers[id] = { selected, custom };\n }\n return answers;\n}\n\n/**\n * \u8FD9\u4E00\u8F6E\u91CC\u4E00\u9053\u9898\u7684\u4F5C\u7B54\uFF0C\u9009\u4E2D\u7684\u9879\u6362\u56DE\u7ED9\u4EBA\u770B\u7684\u6587\u5B57\uFF08\u4E0D\u5E26\u300C\uFF08\u63A8\u8350\uFF09\u300D\u540E\u7F00\uFF09\u3002\n * @param {object} question - \u9898\u76EE\u6A21\u578B\u3002\n * @param {{selected?: string[], custom?: string}|undefined} answer - \u8BE5\u9898\u7684\u4F5C\u7B54\u3002\n * @returns {{selected: string[], custom: string}} \u5C55\u793A\u7528\u7684\u4F5C\u7B54\uFF1B\u6CA1\u4F5C\u7B54\u65F6\u4E24\u6837\u90FD\u7A7A\u3002\n */\nexport function answerParts(question, answer) {\n if (!isRecord(answer)) return { selected: [], custom: '' };\n const shown = new Map(question.options.map((option) => [option.label, option.displayLabel]));\n return {\n selected: stringList(answer.selected).map((label) => shown.get(label) ?? label),\n custom: typeof answer.custom === 'string' ? answer.custom.trim() : '',\n };\n}\n\n/**\n * \u4E00\u9053\u9898\u7684\u4F5C\u7B54\u9010\u884C\u5C55\u5F00\uFF08\u9009\u4E2D\u7684\u9879\u5728\u524D\u3001\u81EA\u586B\u7684\u5B57\u5728\u540E\uFF09\u3002\u5BFC\u5165\u6587\u672C\u4E0E\u53EA\u8BFB\u5C55\u793A\u5171\u7528\u8FD9\u4E00\u4EFD\uFF0C\n * \u4E24\u5904\u7684\u884C\u5E8F\u4E0E\u53D6\u820D\u4E0D\u4F1A\u5404\u5199\u4E00\u5957\u3002\n * @param {object} question - \u9898\u76EE\u6A21\u578B\u3002\n * @param {{selected?: string[], custom?: string}|undefined} answer - \u8BE5\u9898\u7684\u4F5C\u7B54\u3002\n * @returns {string[]} \u9010\u884C\u6587\u5B57\uFF1B\u6CA1\u4F5C\u7B54\u65F6\u4E3A\u7A7A\u6570\u7EC4\u3002\n */\nexport function answerLines(question, answer) {\n const { selected, custom } = answerParts(question, answer);\n return [...selected, ...(custom === '' ? [] : [custom])];\n}\n\n/**\n * \u300C\u63A8\u8350\u7684\u9009\u9879\u9ED8\u8BA4\u52FE\u4E0A\u300D\u843D\u5230\u4E00\u4EFD\u4F5C\u7B54\u8868\u4E0A\uFF1A\u7528\u6237\u6CA1\u52A8\u8FC7\u7684\u9898\uFF0C\u6807\u4E86\u63A8\u8350\u7684\u9009\u9879\u5C31\u662F\u9009\u4E2D\u6001\u3002\n * \u7528\u6237\u52A8\u8FC7\uFF08storage \u91CC\u6709\u8FD9\u9898\u7684\u8BB0\u5F55\uFF09\u7684\u9898\u4E00\u5F8B\u4EE5\u4ED6\u7684\u4E3A\u51C6\u2014\u2014\u5305\u62EC\u4ED6\u628A\u9ED8\u8BA4\u9879\u53D6\u6D88\u6389\u3001\u53EA\u7559\u4E00\u4E2A\u7A7A\u8BB0\u5F55\uFF0C\n * \u6240\u4EE5\u8FD9\u91CC\u53EA\u8865**\u7F3A\u5E2D**\u7684\u9898\uFF0C\u7EDD\u4E0D\u8986\u76D6\u5DF2\u6709\u7684\u4F5C\u7B54\u3002\n * @param {object[]} questions - \u9898\u76EE\u6A21\u578B\u3002\n * @param {object} answers - \u7528\u6237\u81EA\u5DF1\u7684\u4F5C\u7B54\u8868\uFF08readStoredForm \u7684\u4EA7\u7269\uFF09\u3002\n * @returns {object} \u8868\u5355\u4E0A\u771F\u6B63\u663E\u793A\u3001\u5BFC\u5165\u3001\u8BA1\u6570\u7684\u4F5C\u7B54\u8868\u3002\n */\nexport function applyRecommendedDefaults(questions, answers) {\n const merged = { ...(isRecord(answers) ? answers : {}) };\n for (const question of questions) {\n if (merged[question.id] !== undefined) continue;\n const selected = question.options.filter((option) => option.recommended).map((option) => option.label);\n if (selected.length > 0) merged[question.id] = { selected, custom: '' };\n }\n return merged;\n}\n\n/**\n * \u751F\u6210\u5BFC\u5165\u6587\u672C\u7684\u6B63\u6587\uFF1A\u9010\u9898\u4E00\u884C\u300C\u6807\u9898\uFF1A\u4F60\u9009\u7684\u9879\uFF0F\u4F60\u81EA\u586B\u7684\u5B57\u300D\uFF0C\u53EA\u542B\u6709\u4F5C\u7B54\u7684\u9898\u3002\n * \u4E00\u884C\u90FD\u6CA1\u6709\u65F6\u8FD4\u56DE\u7A7A\u4E32\uFF08\u8C03\u7528\u65B9\u636E\u6B64\u7981\u7528\u5BFC\u5165\u3001\u4E5F\u4E0D\u663E\u793A\u672A\u5BFC\u5165\u63D0\u9192\uFF09\u3002\n * @param {object[]} questions - \u9898\u76EE\u6A21\u578B\u3002\n * @param {object} answers - \u4F5C\u7B54\u8868\u3002\n * @returns {string} \u6B63\u6587\uFF08\u4E0D\u542B\u5BFC\u5165\u5757\u7684\u8D77\u6B62\u884C\uFF09\u3002\n */\nexport function importBody(questions, answers) {\n const lines = [];\n for (const question of questions) {\n const answer = answerLines(question, answers[question.id]);\n if (answer.length === 0) continue;\n lines.push(`${titleOf(question)}\uFF1A${answer.join('\u3001')}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * \u5BFC\u5165\u5757\u5728\u8349\u7A3F\u91CC\u7684\u4F4D\u7F6E\u3002\u8D77\u6B62\u884C\u4E0D\u914D\u5BF9\u65F6\u6309\u6CA1\u6709\u5757\u5904\u7406\u3002\n * @param {unknown} draft - \u8F93\u5165\u6846\u8349\u7A3F\u3002\n * @returns {{start: number, end: number}|undefined} \u5757\u7684\u7EDD\u5BF9\u533A\u95F4\uFF1Bend \u662F\u6536\u5C3E\u884C\u4E4B\u540E\u7684\u4F4D\u7F6E\u3002\n */\nfunction findImportBlock(draft) {\n if (typeof draft !== 'string') return undefined;\n const start = draft.indexOf(IMPORT_BLOCK_START);\n if (start === -1) return undefined;\n const stop = draft.indexOf(IMPORT_BLOCK_END, start + IMPORT_BLOCK_START.length);\n if (stop === -1) return undefined;\n return { start, end: stop + IMPORT_BLOCK_END.length };\n}\n\nfunction blockTextOf(body) {\n return `${IMPORT_BLOCK_START}\\n${body}\\n${IMPORT_BLOCK_END}`;\n}\n\n/**\n * \u628A\u4F5C\u7B54\u5BFC\u5165\u8349\u7A3F\uFF1A\u8349\u7A3F\u91CC\u5DF2\u6709\u5BFC\u5165\u5757\u5C31\u539F\u5730\u66FF\u6362\u5B83\u672C\u8EAB\uFF08\u5757\u5916\u7684\u5B57\u4E00\u4E2A\u4E0D\u52A8\uFF09\uFF0C\n * \u6CA1\u6709\u5C31\u628A\u5757\u653E\u5728\u6700\u524D\u9762\u3001\u7528\u6237\u539F\u6709\u7684\u6587\u5B57\u6574\u6BB5\u63A5\u5728\u540E\u9762\u3002\n * @param {unknown} draft - \u5F53\u524D\u8349\u7A3F\u3002\n * @param {string} body - importBody \u7684\u4EA7\u7269\uFF1B\u7A7A\u4E32\u65F6\u539F\u6837\u8FD4\u56DE\u8349\u7A3F\u3002\n * @returns {string} \u65B0\u7684\u8349\u7A3F\u6587\u672C\u3002\n */\nexport function applyImport(draft, body) {\n const text = typeof draft === 'string' ? draft : '';\n if (body === '') return text;\n const block = blockTextOf(body);\n const current = findImportBlock(text);\n if (current !== undefined) return text.slice(0, current.start) + block + text.slice(current.end);\n return text === '' ? block : `${block}\\n\\n${text}`;\n}\n\n/**\n * \u8349\u7A3F\u91CC\u90A3\u6BB5\u5BFC\u5165\u5757\u73B0\u5728\u662F\u4E0D\u662F\u5C31\u662F\u8FD9\u4EFD\u4F5C\u7B54\u3002\u8868\u5355\u586B\u4E86\u4E1C\u897F\u5374\u6CA1\u5BFC\u8FDB\u53BB\uFF08\u6216\u5BFC\u5165\u540E\u53C8\u6539\u4E86\uFF09\n * \u5C31\u4E3A true\u2014\u2014\u754C\u9762\u636E\u6B64\u663E\u793A\u672A\u5BFC\u5165\u63D0\u9192\u3002\u5E73\u53F0\u6CA1\u6709\u53D1\u9001\u524D\u7684\u63D0\u4EA4\u94A9\u5B50\uFF0C\u63D0\u9192\u53EA\u80FD\u662F\u63D0\u9192\u3002\n * @param {unknown} draft - \u5F53\u524D\u8349\u7A3F\u3002\n * @param {string} body - importBody \u7684\u4EA7\u7269\uFF1B\u7A7A\u4E32\uFF08\u8868\u5355\u91CC\u4EC0\u4E48\u90FD\u6CA1\u6709\uFF09\u65F6\u6052\u4E3A false\u3002\n * @returns {boolean} \u662F\u5426\u9700\u8981\u63D0\u9192\u3002\n */\nexport function importStale(draft, body) {\n if (body === '') return false;\n const current = findImportBlock(draft);\n if (current === undefined) return true;\n const inner = draft.slice(current.start + IMPORT_BLOCK_START.length, current.end - IMPORT_BLOCK_END.length);\n return inner.replace(/^\\n/, '').replace(/\\n$/, '') !== body;\n}\n\n/**\n * \u8FD9\u4E00\u8F6E\u662F\u5426\u5DF2\u7ECF\u8FC7\u53BB\uFF1A\u8BB0\u5F55\u91CC\u51FA\u73B0\u4E86\u8FD9\u6761\u8C03\u7528\u4E4B\u540E\uFF08seq \u66F4\u5927\uFF09\u7684\u7528\u6237\u53D1\u8A00\u3002\n * \u8BA4\u4E0D\u51FA\u8FD9\u6761\u8C03\u7528\u3001\u6216\u754C\u9762\u8FD8\u62FF\u4E0D\u5230\u8BB0\u5F55\u65F6\u4E00\u5F8B\u8FD4\u56DE false\u2014\u2014\u628A\u8868\u5355\u5F53\u53EA\u8BFB\u5361\u4F4F\u6BD4\u591A\u663E\u793A\u4E00\u6B21\u53EF\u7F16\u8F91\n * \u66F4\u7CDF\uFF08\u7B54\u6848\u6CA1\u5730\u65B9\u5199\uFF09\u3002\n * @param {unknown} nodes - chat \u89C6\u56FE\u7684 legacy.nodes\u3002\n * @param {unknown} callId - \u672C\u6B21\u5DE5\u5177\u8C03\u7528\u7684 callId\u3002\n * @returns {boolean} \u662F\u5426\u8F6C\u53EA\u8BFB\u3002\n */\nexport function isReadOnlyAfter(nodes, callId) {\n if (!Array.isArray(nodes)) return false;\n const mine = nodes.find((node) => isRecord(node) && node.callId === callId);\n if (mine === undefined || typeof mine.seq !== 'number') return false;\n return nodes.some((node) => (\n isRecord(node)\n && (node.kind === 'user' || node.kind === 'steering')\n && typeof node.seq === 'number'\n && node.seq > mine.seq\n ));\n}\n\n// --- \u6301\u4E45\u5316\uFF1Astorage \u7531\u8C03\u7528\u65B9\u4F20\u8FDB\u6765\uFF08\u6D4F\u89C8\u5668\u662F localStorage\uFF0C\u6D4B\u8BD5\u662F\u5047\u4EF6\uFF09\uFF0C\u672C\u6A21\u5757\u4E0D\u81EA\u5DF1\u53D6\u5168\u5C40 ---\n\n/**\n * \u8868\u5355\u72B6\u6001\u5728 storage \u91CC\u7684\u952E\uFF1A\u6309\u4F1A\u8BDD\u5206\u4E00\u4EFD\uFF0C\u4E00\u4EFD\u91CC\u6309 callId \u5B58\u6BCF\u4E00\u8F6E\u7684\u52FE\u9009\u4E0E\u81EA\u586B\u3002\n * @param {unknown} sessionId - \u5F53\u524D\u4F1A\u8BDD id\u3002\n * @returns {string} storage \u952E\u3002\n */\nexport function formStorageKey(sessionId) {\n const scope = typeof sessionId === 'string' && sessionId !== '' ? sessionId : 'unknown-session';\n return `dsh-grilling-form.${scope}`;\n}\n\nfunction readStorage(storage, key) {\n if (storage === undefined || storage === null || typeof storage.getItem !== 'function') return {};\n try {\n const parsed = parseJson(storage.getItem(key));\n return isRecord(parsed) ? parsed : {};\n } catch {\n return {};\n }\n}\n\n/**\n * \u8BFB\u56DE\u672C\u8F6E\u7684\u4F5C\u7B54\u3002storage \u4E0D\u53EF\u7528\u3001\u952E\u4E0D\u5B58\u5728\u3001\u503C\u5F62\u72B6\u4E0D\u5BF9\u90FD\u53EA\u662F\u300C\u4EC0\u4E48\u90FD\u6CA1\u9009\u300D\u3002\n * @param {object|undefined} storage - \u5E26 getItem/setItem \u7684\u5B58\u50A8\uFF08\u53EF\u4E3A undefined\uFF09\u3002\n * @param {string} key - formStorageKey \u7684\u4EA7\u7269\u3002\n * @param {string} callId - \u672C\u6B21\u8C03\u7528\u7684 callId\u3002\n * @returns {object} \u4F5C\u7B54\u8868\u3002\n */\nexport function readStoredForm(storage, key, callId) {\n const all = readStorage(storage, key);\n return sanitizeAnswers(all[callId]);\n}\n\n/**\n * \u5B58\u4E0B\u672C\u8F6E\u7684\u4F5C\u7B54\uFF0C\u4E0D\u52A8\u540C\u4E00\u4E2A\u4F1A\u8BDD\u91CC\u522B\u7684\u8F6E\u6B21\u3002\u5B58\u4E0D\u8FDB\u53BB\uFF08\u914D\u989D\u3001\u9690\u79C1\u6A21\u5F0F\uFF09\u53EA\u8BA9\u8FD9\u4E00\u4EFD\u6D3B\u5728\u5185\u5B58\u91CC\u3002\n * @param {object|undefined} storage - \u5E26 getItem/setItem \u7684\u5B58\u50A8\uFF08\u53EF\u4E3A undefined\uFF09\u3002\n * @param {string} key - formStorageKey \u7684\u4EA7\u7269\u3002\n * @param {string} callId - \u672C\u6B21\u8C03\u7528\u7684 callId\u3002\n * @param {object} answers - \u4F5C\u7B54\u8868\u3002\n * @returns {void}\n */\nexport function writeStoredForm(storage, key, callId, answers) {\n if (storage === undefined || storage === null || typeof storage.setItem !== 'function') return;\n const all = readStorage(storage, key);\n try {\n storage.setItem(key, JSON.stringify({ ...all, [callId]: sanitizeAnswers(answers) }));\n } catch {\n // \u914D\u989D\u6EE1 / \u9690\u79C1\u6A21\u5F0F\uFF1A\u8868\u5355\u7167\u5E38\u5728\u5185\u5B58\u91CC\u80FD\u586B\uFF0C\u53EA\u662F\u5237\u65B0\u540E\u4E0D\u6062\u590D\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Grilling form",
4
+ "description": "Puts a whole round of grilling questions into the conversation as one interactive form, answered through the native composer."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "盘问表单",
4
+ "description": "把一整轮盘问问题作为一张可交互表单投进对话,作答经原生输入框带回。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@lynn123411/dsh-ask-user-grilling",
3
- "version": "0.6.3",
4
- "description": "Form-presentation variant of the native ask_user_question: same tool description and shared-parameter descriptions verbatim, forced multi-select, an optional question number merged into the header, an optional markdown detail body, a recommendation marker normalized into the one label suffix the client renders, plus an auto-appended round-end supplement question. Its own browser half renders the transcript card for ask_user_grilling (question list, detail markdown, round-end supplement, rejected-round violations).",
3
+ "version": "0.7.0",
4
+ "description": "Grilling-round question tool: puts the whole round of questions into the conversation as an interactive form and ends the turn there — the user answers it in the native composer and their next message carries the answers back. Its browser half lays that form flat in the transcript (no collapse row, no card shell) with forced multi-select, recommended options pre-checked, per-question free text, detail markdown, an import-to-composer button, an unsent-content reminder, and read-only once the round is over.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
8
8
  ".": "./lib/index.js",
9
9
  "./client": "./lib/client.js",
10
10
  "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./locale/en.json": "./locale/en.json",
12
+ "./locale/zh.json": "./locale/zh.json",
11
13
  "./package.json": "./package.json"
12
14
  },
13
15
  "files": [
14
16
  "lib",
15
17
  "cordis.patch.yml",
16
- "README.md"
18
+ "README.md",
19
+ "locale",
20
+ "assets"
17
21
  ],
18
22
  "license": "MIT",
19
23
  "dsh": {
@@ -30,32 +34,29 @@
30
34
  "scripts": {
31
35
  "build": "node build.mjs",
32
36
  "prepack": "pnpm run build",
33
- "test": "node scripts/test-recommendation.mjs && node scripts/test-card.mjs && node scripts/smoke-host.mjs && node scripts/test-render.mjs && node scripts/smoke-client.mjs",
37
+ "test": "node scripts/test-recommendation.mjs && node scripts/test-form-model.mjs && node scripts/smoke-host.mjs && node scripts/test-render.mjs && node scripts/smoke-client.mjs",
34
38
  "assert:lib-untracked": "node scripts/assert-lib-untracked.mjs",
35
39
  "check": "pnpm build && pnpm test && pnpm assert:lib-untracked"
36
40
  },
37
41
  "peerDependencies": {
38
42
  "@deepseek-ai/cordis": "^4.0.4",
39
- "@deepseek-ai/dsh-tools": ">=0.1.7-rc.2",
40
- "@deepseek-ai/dsh-user-questions": ">=0.1.7-rc.2"
43
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.2"
41
44
  },
42
45
  "devDependencies": {
43
46
  "@deepseek-ai/cordis": "4.0.4",
44
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.2",
45
- "@deepseek-ai/dsh-client-ui-user-questions": ">=0.1.7-rc.2",
46
- "@deepseek-ai/dsh-system-prompt": ">=0.1.7-rc.2",
47
- "@deepseek-ai/dsh-tool-ask-user": ">=0.1.7-rc.2",
48
- "@deepseek-ai/dsh-tools": ">=0.1.7-rc.2",
49
- "@deepseek-ai/dsh-user-questions": ">=0.1.7-rc.2",
47
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.2",
48
+ "@deepseek-ai/dsh-system-prompt": ">=0.2.0-rc.2",
49
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.2",
50
50
  "esbuild": "^0.25.0",
51
51
  "react": "^18.3.1",
52
52
  "react-dom": "^18.3.1"
53
53
  },
54
54
  "engines": {
55
- "dsh": ">=0.1.7-rc.2",
55
+ "dsh": ">=0.2.0-rc.2",
56
56
  "node": "^22.19 || >=24"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"
60
- }
60
+ },
61
+ "icon": "./assets/icon.svg"
61
62
  }