@changfenhuang/dsh-genui 0.11.1 → 0.11.2

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/SKILL.md +4 -2
  5. package/lib/assets/standalone-runtime.js +1505 -0
  6. package/lib/client.js +163 -25
  7. package/lib/index.js +222 -18
  8. package/lib/types/client/artifact/ExportableGenuiBlock.d.ts +6 -0
  9. package/lib/types/client/artifact/create.d.ts +5 -0
  10. package/lib/types/client/artifact/download.d.ts +9 -0
  11. package/lib/types/client/artifact/html.d.ts +5 -0
  12. package/lib/types/client/artifact/index.d.ts +8 -0
  13. package/lib/types/client/artifact/json.d.ts +3 -0
  14. package/lib/types/client/artifact/parse.d.ts +3 -0
  15. package/lib/types/client/artifact/portability.d.ts +4 -0
  16. package/lib/types/client/artifact/standalone-theme.d.ts +2 -0
  17. package/lib/types/client/artifact/types.d.ts +33 -0
  18. package/lib/types/client/blocks/state.d.ts +2 -0
  19. package/lib/types/client/echarts-engine.d.ts +4 -0
  20. package/lib/types/client/echarts-lazy.d.ts +1 -3
  21. package/lib/types/client/embed.d.ts +2 -0
  22. package/lib/types/client/i18n/en.d.ts +6 -0
  23. package/lib/types/client/primitive-adapter.d.ts +4 -0
  24. package/lib/types/client/primitive-labels.d.ts +1 -1
  25. package/lib/types/client/standalone/asset-loader.d.ts +2 -0
  26. package/lib/types/client/standalone/bootstrap-types.d.ts +9 -0
  27. package/lib/types/client/standalone/katex-style.d.ts +1 -0
  28. package/lib/types/client/standalone/primitive-adapter.d.ts +54 -0
  29. package/lib/types/client/standalone/runtime.d.ts +2 -0
  30. package/lib/types/plugin/fence-feedback.d.ts +4 -1
  31. package/lib/types/plugin/index.d.ts +1 -1
  32. package/lib/types/plugin/inline-content-diagnostic.d.ts +8 -0
  33. package/package.json +3 -2
  34. package/src/client/EChartNode.tsx +4 -8
  35. package/src/client/GenuiBlock.tsx +17 -13
  36. package/src/client/action-context.ts +1 -1
  37. package/src/client/artifact/ArtifactExport.module.css +64 -0
  38. package/src/client/artifact/ExportableGenuiBlock.tsx +100 -0
  39. package/src/client/artifact/create.ts +69 -0
  40. package/src/client/artifact/download.ts +42 -0
  41. package/src/client/artifact/html.ts +157 -0
  42. package/src/client/artifact/index.ts +15 -0
  43. package/src/client/artifact/json.ts +6 -0
  44. package/src/client/artifact/parse.ts +32 -0
  45. package/src/client/artifact/portability.ts +47 -0
  46. package/src/client/artifact/standalone-theme.ts +94 -0
  47. package/src/client/artifact/types.ts +44 -0
  48. package/src/client/asset-loader.ts +8 -0
  49. package/src/client/blocks/advanced.tsx +9 -1
  50. package/src/client/blocks/charts.tsx +1 -1
  51. package/src/client/blocks/forms.tsx +8 -5
  52. package/src/client/blocks/render-node.tsx +1 -1
  53. package/src/client/blocks/state.ts +2 -0
  54. package/src/client/echarts-engine.ts +9 -0
  55. package/src/client/echarts-lazy.ts +1 -4
  56. package/src/client/embed.ts +10 -0
  57. package/src/client/fence-render.tsx +5 -13
  58. package/src/client/genui-runtime/normalize.ts +1 -1
  59. package/src/client/guard.ts +2 -1
  60. package/src/client/i18n/en.ts +8 -0
  61. package/src/client/i18n/zh.ts +8 -0
  62. package/src/client/inline.ts +26 -1
  63. package/src/client/panel.tsx +2 -2
  64. package/src/client/primitive-adapter.ts +13 -0
  65. package/src/client/primitive-labels.ts +1 -1
  66. package/src/client/standalone/asset-loader.ts +9 -0
  67. package/src/client/standalone/bootstrap-types.ts +11 -0
  68. package/src/client/standalone/katex-style.ts +1 -0
  69. package/src/client/standalone/primitive-adapter.tsx +94 -0
  70. package/src/client/standalone/runtime.tsx +27 -0
  71. package/src/client/toolview.tsx +2 -2
  72. package/src/plugin/fence-feedback.ts +25 -9
  73. package/src/plugin/index.ts +4 -3
  74. package/src/plugin/inline-content-diagnostic.ts +178 -0
  75. package/src/plugin/tool.ts +18 -3
  76. package/tsdown.config.ts +82 -1
package/lib/index.js CHANGED
@@ -968,7 +968,7 @@ function normalizeNode(value, path, warnings) {
968
968
  canonical: "items"
969
969
  });
970
970
  }
971
- normalizedHolder.items = Array.isArray(normalizedHolder.items) ? normalizedHolder.items.map((child, childIndex) => normalizeNodeValue(child, `${path}.tabs[${index}].items[${childIndex}]`)) : normalizedHolder.items === void 0 ? normalizedHolder.items : [normalizeNodeValue(normalizedHolder.items, `${path}.tabs[${index}].items[0]`)];
971
+ normalizedHolder.items = Array.isArray(normalizedHolder.items) ? normalizedHolder.items.map((child, childIndex) => normalizeNodeValue(child, `${path}.tabs[${index}].items[${childIndex}]`)) : normalizedHolder.items === void 0 ? [] : [normalizeNodeValue(normalizedHolder.items, `${path}.tabs[${index}].items[0]`)];
972
972
  return normalizedHolder;
973
973
  });
974
974
  else if (type === "accordion" && Array.isArray(out.items)) out.items = out.items.map((item, index) => {
@@ -2618,9 +2618,10 @@ function validateCanonicalGenuiSpec(value) {
2618
2618
  }
2619
2619
  };
2620
2620
  walk(v.items, 0, "items");
2621
+ const uniqueErrors = [...new Set(errors)];
2621
2622
  return {
2622
- ok: errors.length === 0,
2623
- errors
2623
+ ok: uniqueErrors.length === 0,
2624
+ errors: uniqueErrors
2624
2625
  };
2625
2626
  }
2626
2627
  /**
@@ -3426,6 +3427,188 @@ function droppedNodeFailure(processed, raw) {
3426
3427
  ];
3427
3428
  }
3428
3429
  //#endregion
3430
+ //#region src/plugin/inline-content-diagnostic.ts
3431
+ /** 识别文字字段中的连续代码围栏标记。 */
3432
+ function hasFencedCode(value) {
3433
+ return /`{3,}|~{3,}/.test(value);
3434
+ }
3435
+ /** 判断相邻表头下方的 Markdown 表格分隔行。 */
3436
+ function isMarkdownTableSeparator(line) {
3437
+ let value = line.trim();
3438
+ if (value.startsWith("|")) value = value.slice(1);
3439
+ if (value.endsWith("|")) value = value.slice(0, -1);
3440
+ const cells = value.split("|").map((cell) => cell.trim());
3441
+ return cells.length >= 2 && cells.every((cell) => /^:?-{2,}:?$/.test(cell));
3442
+ }
3443
+ /** 仅在相邻表头和分隔行构成表格时报告。 */
3444
+ function hasMarkdownTable(value) {
3445
+ const lines = value.split(/\r?\n/);
3446
+ for (let i = 0; i + 1 < lines.length; i++) if (lines[i].includes("|") && isMarkdownTableSeparator(lines[i + 1])) return true;
3447
+ return false;
3448
+ }
3449
+ /** 按组件结构检查需要块级内容诊断的 canonical 显示字段。 */
3450
+ function collectInlineContentWarnings(spec) {
3451
+ const warnings = [];
3452
+ /** 将一个可见文字字段中的块级 Markdown 记录为稳定诊断。 */
3453
+ function check(value, path) {
3454
+ if (value === void 0) return;
3455
+ if (hasFencedCode(value)) warnings.push({
3456
+ path,
3457
+ kind: "fenced_code",
3458
+ replacement: "code"
3459
+ });
3460
+ if (hasMarkdownTable(value)) warnings.push({
3461
+ path,
3462
+ kind: "markdown_table",
3463
+ replacement: "table"
3464
+ });
3465
+ }
3466
+ /** 只沿 GenUI 组件的子节点字段访问下一层。 */
3467
+ function visit(node, path) {
3468
+ switch (node.type) {
3469
+ case "text":
3470
+ check(node.content, `${path}.content`);
3471
+ break;
3472
+ case "card":
3473
+ check(node.title, `${path}.title`);
3474
+ node.items.forEach((child, i) => visit(child, `${path}.items[${i}]`));
3475
+ break;
3476
+ case "row":
3477
+ case "col":
3478
+ case "grid":
3479
+ node.items.forEach((child, i) => visit(child, `${path}.items[${i}]`));
3480
+ break;
3481
+ case "button":
3482
+ case "checkbox":
3483
+ case "link":
3484
+ case "badge":
3485
+ case "switch":
3486
+ case "submit":
3487
+ check(node.label, `${path}.label`);
3488
+ break;
3489
+ case "input":
3490
+ case "select":
3491
+ case "slider":
3492
+ case "textarea":
3493
+ check(node.label, `${path}.label`);
3494
+ break;
3495
+ case "radio":
3496
+ check(node.label, `${path}.label`);
3497
+ node.options.forEach((option, i) => check(option, `${path}.options[${i}]`));
3498
+ if (typeof node.answer === "string") check(node.answer, `${path}.answer`);
3499
+ check(node.explanation, `${path}.explanation`);
3500
+ break;
3501
+ case "image":
3502
+ case "audio":
3503
+ case "video":
3504
+ check(node.alt, `${path}.alt`);
3505
+ break;
3506
+ case "hero":
3507
+ for (const field of [
3508
+ "label",
3509
+ "value",
3510
+ "delta",
3511
+ "title",
3512
+ "subtitle"
3513
+ ]) check(node[field], `${path}.${field}`);
3514
+ break;
3515
+ case "stat":
3516
+ for (const field of [
3517
+ "label",
3518
+ "value",
3519
+ "delta"
3520
+ ]) check(node[field], `${path}.${field}`);
3521
+ break;
3522
+ case "progress":
3523
+ check(node.label, `${path}.label`);
3524
+ check(node.valueLabel, `${path}.valueLabel`);
3525
+ break;
3526
+ case "list":
3527
+ node.items.forEach((item, i) => {
3528
+ const itemPath = `${path}.items[${i}]`;
3529
+ if (typeof item === "string") check(item, itemPath);
3530
+ else if ("type" in item && typeof item.type === "string") visit(item, itemPath);
3531
+ else {
3532
+ check(item.title, `${itemPath}.title`);
3533
+ check(item.desc, `${itemPath}.desc`);
3534
+ }
3535
+ });
3536
+ break;
3537
+ case "table":
3538
+ node.columns.forEach((column, i) => check(column, `${path}.columns[${i}]`));
3539
+ node.rows.forEach((row, i) => {
3540
+ const detail = node.details?.[i] ?? null;
3541
+ row.forEach((cell, j) => {
3542
+ if (node.types?.[j] === "index" && (j !== 0 || detail === null)) return;
3543
+ if (typeof cell === "string") check(cell, `${path}.rows[${i}][${j}]`);
3544
+ });
3545
+ });
3546
+ node.details?.forEach((detail, i) => detail?.forEach((child, j) => visit(child, `${path}.details[${i}][${j}]`)));
3547
+ break;
3548
+ case "chart":
3549
+ node.series?.forEach((series, i) => check(series.label, `${path}.series[${i}].label`));
3550
+ break;
3551
+ case "tabs":
3552
+ node.tabs.forEach((tab, i) => {
3553
+ check(tab.label, `${path}.tabs[${i}].label`);
3554
+ tab.items.forEach((child, j) => visit(child, `${path}.tabs[${i}].items[${j}]`));
3555
+ });
3556
+ break;
3557
+ case "accordion":
3558
+ node.items.forEach((item, i) => {
3559
+ check(item.title, `${path}.items[${i}].title`);
3560
+ item.items.forEach((child, j) => visit(child, `${path}.items[${i}].items[${j}]`));
3561
+ });
3562
+ break;
3563
+ case "callout":
3564
+ check(node.title, `${path}.title`);
3565
+ check(node.content, `${path}.content`);
3566
+ break;
3567
+ case "steps":
3568
+ node.steps.forEach((step, i) => {
3569
+ check(step.title, `${path}.steps[${i}].title`);
3570
+ check(step.desc, `${path}.steps[${i}].desc`);
3571
+ });
3572
+ break;
3573
+ case "keyvalue":
3574
+ node.pairs.forEach((pair, i) => {
3575
+ check(pair.key, `${path}.pairs[${i}].key`);
3576
+ check(pair.value, `${path}.pairs[${i}].value`);
3577
+ });
3578
+ break;
3579
+ case "timeline":
3580
+ node.items.forEach((item, i) => {
3581
+ check(item.title, `${path}.items[${i}].title`);
3582
+ check(item.time, `${path}.items[${i}].time`);
3583
+ check(item.desc, `${path}.items[${i}].desc`);
3584
+ });
3585
+ break;
3586
+ case "breadcrumb":
3587
+ node.items.forEach((item, i) => check(item, `${path}.items[${i}]`));
3588
+ break;
3589
+ case "quiz":
3590
+ check(node.question, `${path}.question`);
3591
+ node.options.forEach((option, i) => {
3592
+ check(option.label, `${path}.options[${i}].label`);
3593
+ check(option.feedback, `${path}.options[${i}].feedback`);
3594
+ });
3595
+ check(node.explanation, `${path}.explanation`);
3596
+ break;
3597
+ case "plot":
3598
+ case "echart":
3599
+ case "diagram":
3600
+ case "scene3d":
3601
+ case "svg":
3602
+ check(node.title, `${path}.title`);
3603
+ break;
3604
+ case "copy": check(node.label, `${path}.label`);
3605
+ }
3606
+ }
3607
+ check(spec.title, "title");
3608
+ spec.items.forEach((node, i) => visit(node, `items[${i}]`));
3609
+ return warnings;
3610
+ }
3611
+ //#endregion
3429
3612
  //#region src/plugin/tool.ts
3430
3613
  /**
3431
3614
  * Arguments schema: an open `spec` slot. The schema must NOT reject anything
@@ -3557,6 +3740,10 @@ function formatProcessWarnings(processed) {
3557
3740
  return `warning=process detail=${JSON.stringify(warning.message)}`;
3558
3741
  });
3559
3742
  }
3743
+ /** 将行内字段的块级 Markdown 诊断写入稳定的验证协议。 */
3744
+ function formatInlineContentWarnings(spec) {
3745
+ return collectInlineContentWarnings(spec).map((warning) => `warning=block_markdown path=${warning.path} kind=${warning.kind} replacement=${warning.replacement}`);
3746
+ }
3560
3747
  /** Format chart-specific process errors while keeping other schema errors generic. */
3561
3748
  function formatProcessFailure(processed) {
3562
3749
  const chartErrors = processed.errors.filter((error) => /(?:variant is unsupported|kind must be bars, line, or donut|requires data or series|(?:data|series) is required for|(?:\.data|\.series)(?:\[\d+\])?(?:\.(?:data|label|value|color))? must|series is only supported for bars)/.test(error));
@@ -3644,7 +3831,7 @@ function createRenderUiTool() {
3644
3831
  * before emitting — turning "render a red banner after the fact" into
3645
3832
  * "verify before you send". Purely local: no LLM, no network, no DOM.
3646
3833
  */
3647
- const VALIDATE_DESCRIPTION = "Validate the JSON body of a ```dsh-ui fence BEFORE emitting it — use for non-trivial specs (≥3 nodes or containing a table); skip for trivial ones (≤2 nodes). Pass the exact JSON text you are about to put inside the fence as the \"spec\" argument (a string). Returns a [genui-validation] protocol block with status, diagnostics, next action, and reply_language=conversation. When invalid JSON is repairable, next=emit_repaired_fence and repaired_json contain the exact fence body to emit.";
3834
+ const VALIDATE_DESCRIPTION = "Validate the JSON body of a ```dsh-ui fence BEFORE emitting it — use for non-trivial specs (≥3 nodes or containing a table); skip for trivial ones (≤2 nodes). Pass the exact JSON text you are about to put inside the fence as the \"spec\" argument (a string). Returns a [genui-validation] protocol block with status, diagnostics, next action, and reply_language=conversation. When invalid JSON is repairable, repaired_json contains the fixed body; emit it only with next=emit_repaired_fence. A warning=block_markdown requires the indicated replacement node and another validation call.";
3648
3835
  const VALIDATE_PARAMETERS = {
3649
3836
  type: "object",
3650
3837
  properties: { spec: {
@@ -3759,15 +3946,17 @@ function createValidateDshUiTool() {
3759
3946
  if (chartFailure !== void 0) return chartFailure;
3760
3947
  if (processed.spec !== null && processed.errors.length === 0) {
3761
3948
  const warnings = formatProcessWarnings(processed);
3949
+ const inlineWarnings = formatInlineContentWarnings(processed.spec);
3762
3950
  return `${validationProtocol([
3763
3951
  "status=invalid",
3764
3952
  "error=invalid_json",
3765
3953
  `detail=${JSON.stringify(detail)}`,
3766
3954
  ...bracketDiagnostic(raw),
3767
3955
  ...warnings,
3956
+ ...inlineWarnings,
3768
3957
  "repair=applied",
3769
3958
  `repair_count=${repaired.repairs}`,
3770
- "next=emit_repaired_fence"
3959
+ inlineWarnings.length === 0 ? "next=emit_repaired_fence" : "next=fix_and_revalidate"
3771
3960
  ])}\nrepaired_json:\n\`\`\`\n${repaired.text}\n\`\`\``;
3772
3961
  }
3773
3962
  }
@@ -3798,11 +3987,13 @@ function createValidateDshUiTool() {
3798
3987
  ]);
3799
3988
  }
3800
3989
  const warnings = formatProcessWarnings(processed);
3990
+ const inlineWarnings = formatInlineContentWarnings(processed.spec);
3801
3991
  return validationProtocol([
3802
3992
  "status=valid",
3803
3993
  `rendered=${processed.renderedCount}`,
3804
3994
  ...warnings,
3805
- "next=emit_fence"
3995
+ ...inlineWarnings,
3996
+ inlineWarnings.length === 0 ? "next=emit_fence" : "next=fix_and_revalidate"
3806
3997
  ]);
3807
3998
  },
3808
3999
  presentCall() {
@@ -3974,6 +4165,8 @@ function resolveFence(raw, options) {
3974
4165
  //#region src/plugin/fence-feedback.ts
3975
4166
  /** Plugin name recorded on every message this loop steers. */
3976
4167
  const FEEDBACK_PLUGIN_NAME = "@changfenhuang/dsh-genui";
4168
+ /** Source kind persisted by this plugin in Session format v4. */
4169
+ const FEEDBACK_SOURCE_KIND = `plugin:${FEEDBACK_PLUGIN_NAME}`;
3977
4170
  /** Marker prefix inside the correction text: `[genui-fence-repair #<fingerprint>]`. */
3978
4171
  const MARKER_PREFIX = "[genui-fence-repair #";
3979
4172
  /** Marker prefix written by older plugin versions. */
@@ -4068,9 +4261,20 @@ function fenceCorrectionText(failures) {
4068
4261
  * copy resolves identically on every host.
4069
4262
  *
4070
4263
  * @param text - the correction text.
4264
+ * @param sessionFormatVersion - the format recorded by the active session.
4071
4265
  * @returns a frozen user message attributed to this plugin as a notice.
4072
4266
  */
4073
- function createFeedbackMessage(text) {
4267
+ function createFeedbackMessage(text, sessionFormatVersion) {
4268
+ const source = sessionFormatVersion >= 4 ? {
4269
+ kind: FEEDBACK_SOURCE_KIND,
4270
+ form: "notice",
4271
+ summary: "genui fence repair requested"
4272
+ } : {
4273
+ kind: "plugin",
4274
+ plugin: FEEDBACK_PLUGIN_NAME,
4275
+ form: "notice",
4276
+ summary: "genui fence repair requested"
4277
+ };
4074
4278
  const message = {
4075
4279
  id: randomUUID(),
4076
4280
  role: "user",
@@ -4078,12 +4282,7 @@ function createFeedbackMessage(text) {
4078
4282
  type: "text",
4079
4283
  text
4080
4284
  }],
4081
- source: {
4082
- kind: "plugin",
4083
- plugin: FEEDBACK_PLUGIN_NAME,
4084
- form: "notice",
4085
- summary: "genui fence repair requested"
4086
- }
4285
+ source
4087
4286
  };
4088
4287
  Object.freeze(message.content);
4089
4288
  return Object.freeze(message);
@@ -4135,6 +4334,10 @@ function markersIn(text) {
4135
4334
  }
4136
4335
  return out;
4137
4336
  }
4337
+ /** Identify this plugin's source across current and migrated session shapes. */
4338
+ function isFeedbackSource(source) {
4339
+ return source?.kind === FEEDBACK_SOURCE_KIND || source?.kind === "plugin" && source.plugin === "@changfenhuang/dsh-genui";
4340
+ }
4138
4341
  /**
4139
4342
  * 根据插件配置启用围栏反馈流程。
4140
4343
  *
@@ -4173,7 +4376,7 @@ function installFenceFeedback(ctx, enabled) {
4173
4376
  }
4174
4377
  if (event.type !== "user/message") return;
4175
4378
  const data = event.data;
4176
- if (data.source?.kind === "plugin" && data.source.plugin === "@changfenhuang/dsh-genui") {
4379
+ if (isFeedbackSource(data.source)) {
4177
4380
  const fingerprints = markersIn(textOfContent(data.content));
4178
4381
  if (fingerprints.length === 0) return;
4179
4382
  const state = stateOf(sessionId);
@@ -4198,7 +4401,7 @@ function installFenceFeedback(ctx, enabled) {
4198
4401
  for (const fingerprint of plan.fingerprints) state.corrected.add(fingerprint);
4199
4402
  state.lastCorrectedTurn = plan.turn;
4200
4403
  try {
4201
- agent.steer(createFeedbackMessage(plan.text));
4404
+ agent.steer(createFeedbackMessage(plan.text, agent.session.header.version));
4202
4405
  } catch (error) {
4203
4406
  ctx.logger?.warn?.(`dsh-genui: fence feedback steering failed (${error instanceof Error ? error.message : String(error)})`);
4204
4407
  }
@@ -4281,8 +4484,8 @@ Allowed \`type\` values; the \`genui\` skill, when available, carries the full c
4281
4484
  **默认就该出 UI**:出现下列情况至少出一个围栏:
4282
4485
  - ≥3 条并列要点 → \`list\`;数字对比 → \`table\`;指标/进度/状态 → \`stat\`/\`progress\`/\`badge\`
4283
4486
  - 步骤/时间线 → \`steps\`/\`timeline\`/\`mermaid\`;架构/流程 → \`diagram\` 或 \`mermaid\`;风险/结论 → \`callout\`;代码/改动 → \`code\`/\`diff\`/\`json\`
4284
- - 行内富文本:\`text\`/\`list\`/表格文本列/\`keyvalue\`/\`callout\` 里可写 $公式$、$$块公式$$、\`code\`、**加粗**、==高亮==、[文字](url)。
4285
- - 默认无卡 ≠ 少用组件:硬触发照常出组件,**组件多不是问题**——判据是每个组件承载不同信息、有焦点与层次。卡片只用于并排项与数据对象;单段文字用「标题 + 正文 + 间距」。
4487
+ - 行内富文本:支持公式、\`code\`、加粗、高亮、链接;禁用 Markdown table / fenced code,改用 table / code / diff / json。
4488
+ - 默认无卡:按触发条件使用组件,每个组件承载不同信息;卡片只用于并排项与数据对象,单段文字用标题、正文与间距。
4286
4489
 
4287
4490
  **发回答前最后自检一次**:这段内容里有没有 ≥3 条并列要点、任何对比、任何数字/指标、任何步骤或流程?有就先转成组件再开口。**状态汇报、进度说明、提交与改动清单同样算**。
4288
4491
  - 趋势/占比 → \`chart\`(≤8 点)或 \`echart\`(多序列/要交互时);配色默认跟随主题,只有语义需要时才用 \`palette\`/\`card.accent\`;grid 子节点用 \`"span":2\` 跨列做宽窄混排;数据多时给 \`table\`/\`chart\`/\`list\` 配一个 \`input\`(id) + \`filter\` 绑定,读者能就地筛选。
@@ -4293,7 +4496,8 @@ Allowed \`type\` values; the \`genui\` skill, when available, carries the full c
4293
4496
 
4294
4497
  Rules:
4295
4498
  - LANGUAGE: reply+UI=conversation language; schema fixed. NEVER infer it from prompt/skill/examples/tools. Replace \`<user-language ...>\`; never emit these placeholders literally.
4296
- - JSON 严格: 坏组件被丢弃、坏围栏降级为代码块;≥3 节点或含 table 的围栏发出前必须调 validate_dsh_ui,按结构化诊断修好再发;小围栏字段没把握也必须先验证。
4499
+ - JSON 严格:坏组件被丢弃,坏围栏变代码块;≥3 节点或含 table 时调 validate_dsh_ui,按诊断修改并重验;小围栏字段存疑也先验证。
4500
+ - warning=block_markdown:按 replacement 改写并重验。
4297
4501
  - 规模: ≤200 节点、嵌套≤8 层(超出被截断);一条回答 3–8 个组件,一个主题一个主组件;3D mesh 1–5;plot 给合理 xMin/xMax。
4298
4502
  - LOCAL-FIRST + actions: UI 能自己做的状态变化(判卷、判题、重置、展开、选中)就地完成,零往返;action 只用于必须模型参与的事。交互组件带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI;无 action 的按钮禁用。
4299
4503
  - Durable state: 交互状态按「会话+内容指纹」持久化——刷新/重放恢复;重渲染相同内容保留,新内容重置。
@@ -0,0 +1,6 @@
1
+ import type { GenuiBlockProps } from '../blocks/state.ts';
2
+ export interface ExportableGenuiBlockProps extends GenuiBlockProps {
3
+ exportEnabled?: boolean;
4
+ }
5
+ /** 在完成态 GenUI 外层显示 artifact 导出菜单。 */
6
+ export declare function ExportableGenuiBlock(props: ExportableGenuiBlockProps): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import type { BlockInteractionState } from '../interaction-store.ts';
2
+ import type { GenuiSpec } from '../spec.ts';
3
+ import { type CreateGenuiArtifactOptions, type GenuiArtifactV1 } from './types.ts';
4
+ /** 创建规范化 artifact,并清除密码默认值及对应的持久化字段。 */
5
+ export declare function createGenuiArtifact(spec: GenuiSpec, state?: BlockInteractionState, options?: CreateGenuiArtifactOptions): GenuiArtifactV1;
@@ -0,0 +1,9 @@
1
+ import { type GenuiArtifactV1 } from './types.ts';
2
+ /** 过滤文件名中的路径符号、控制字符和危险标点。 */
3
+ export declare function sanitizeArtifactFilename(title: string | undefined, extension: '.html' | '.genui.json'): string;
4
+ /** 触发浏览器下载并释放临时地址。 */
5
+ export declare function downloadBlob(blob: Blob, filename: string): void;
6
+ /** 下载规范化 GenUI JSON artifact。 */
7
+ export declare function downloadGenuiArtifactJson(artifact: GenuiArtifactV1): void;
8
+ /** 生成并下载独立 HTML。 */
9
+ export declare function downloadGenuiArtifactHtml(artifact: GenuiArtifactV1): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { type GenuiArtifactV1 } from './types.ts';
2
+ /** 使用已下载的运行文件生成完整的独立 HTML 文档。 */
3
+ export declare function createStandaloneHtmlDocument(artifact: GenuiArtifactV1, bundles: ReadonlyMap<string, Uint8Array>, baseURI?: string): string;
4
+ /** 获取独立运行文件及规格实际使用的图形引擎,生成单文件 HTML。 */
5
+ export declare function buildStandaloneHtml(rawArtifact: GenuiArtifactV1): Promise<string>;
@@ -0,0 +1,8 @@
1
+ export { createGenuiArtifact } from './create.ts';
2
+ export { buildStandaloneHtml } from './html.ts';
3
+ export { downloadGenuiArtifactHtml, downloadGenuiArtifactJson } from './download.ts';
4
+ export { parseGenuiArtifact } from './parse.ts';
5
+ export { analyzeGenuiPortability } from './portability.ts';
6
+ export { serializeGenuiArtifact } from './json.ts';
7
+ export { GenuiExportError } from './types.ts';
8
+ export type { CreateGenuiArtifactOptions, GenuiArtifactTheme, GenuiArtifactV1, GenuiExportErrorCode, GenuiPortabilityReport, GenuiStandaloneAsset, } from './types.ts';
@@ -0,0 +1,3 @@
1
+ import type { GenuiArtifactV1 } from './types.ts';
2
+ /** 以稳定缩进序列化 GenUI artifact。 */
3
+ export declare function serializeGenuiArtifact(artifact: GenuiArtifactV1): string;
@@ -0,0 +1,3 @@
1
+ import { type GenuiArtifactV1 } from './types.ts';
2
+ /** 校验 artifact 格式并重新执行当前 GenUI 规范化流程。 */
3
+ export declare function parseGenuiArtifact(value: unknown): GenuiArtifactV1 | null;
@@ -0,0 +1,4 @@
1
+ import type { GenuiSpec } from '../spec.ts';
2
+ import type { GenuiPortabilityReport } from './types.ts';
3
+ /** 扫描嵌套 GenUI 节点,选择运行资源并列出独立页面限制。 */
4
+ export declare function analyzeGenuiPortability(spec: GenuiSpec): GenuiPortabilityReport;
@@ -0,0 +1,2 @@
1
+ /** standalone 页面使用的基础颜色和版面样式。 */
2
+ export declare const STANDALONE_THEME_CSS = "\n:root {\n color-scheme: light;\n --dsw-static-neutral-bluish-00: rgb(255, 255, 255);\n --dsw-static-neutral-bluish-50: rgb(249, 250, 251);\n --dsw-static-neutral-bluish-300: rgb(207, 211, 214);\n --dsw-static-neutral-bluish-400: rgb(173, 178, 184);\n --dsw-static-neutral-bluish-600: rgb(129, 133, 140);\n --dsw-static-neutral-bluish-700: rgb(97, 102, 107);\n --dsw-static-neutral-bluish-800: rgb(53, 54, 56);\n --dsw-static-neutral-bluish-850: rgb(44, 44, 46);\n --dsw-static-neutral-bluish-875: rgb(35, 35, 36);\n --dsw-static-neutral-bluish-900: rgb(27, 27, 28);\n --dsw-static-neutral-bluish-950: rgb(21, 21, 23);\n --dsw-static-neutral-bluish-1000: rgb(15, 17, 21);\n --dsw-alias-bg-base: var(--dsw-static-neutral-bluish-00);\n --dsw-alias-bg-layer-1: var(--dsw-static-neutral-bluish-00);\n --dsw-alias-bg-layer-2: var(--dsw-static-neutral-bluish-00);\n --dsw-alias-bg-layer-3: var(--dsw-static-neutral-bluish-00);\n --dsw-alias-border-l1: rgba(0, 0, 0, 0.04);\n --dsw-alias-border-l2: rgba(0, 0, 0, 0.1);\n --dsw-alias-fill-hover: #eef2f7;\n --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-1000);\n --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-700);\n --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-600);\n --dsw-alias-label-caption: var(--dsw-static-neutral-bluish-400);\n --dsw-alias-markdown-code-block: var(--dsw-static-neutral-bluish-50);\n --dsw-alias-markdown-hr: #cbd5e1;\n /* DSH \u9759\u6001\u8C03\u8272\u677F\u4F9B\u8BED\u4E49\u72B6\u6001\u522B\u540D\u4F7F\u7528\u3002 */\n --dsw-static-deepseek-100: rgb(228, 237, 253);\n --dsw-static-deepseek-400: rgb(122, 170, 255);\n --dsw-static-deepseek-500: rgb(65, 118, 230);\n --dsw-static-deepseek-800: rgb(52, 65, 91);\n --dsw-static-green-100: rgb(230, 250, 237);\n --dsw-static-green-400: rgb(78, 209, 126);\n --dsw-static-green-500: rgb(34, 197, 94);\n --dsw-static-green-900: rgb(35, 60, 44);\n --dsw-static-amber-100: rgb(254, 245, 231);\n --dsw-static-amber-400: rgb(247, 173, 49);\n --dsw-static-amber-500: rgb(245, 158, 11);\n --dsw-static-amber-600: rgb(221, 134, 41);\n --dsw-static-amber-900: rgb(39, 36, 31);\n --dsw-static-red-400: rgb(242, 90, 90);\n --dsw-static-red-600: rgb(236, 19, 19);\n\n /* DSH \u6D45\u8272\u4E3B\u9898\u8BED\u4E49\u72B6\u6001\u522B\u540D\u3002 */\n --dsw-alias-state-business-primary: var(--dsw-static-deepseek-500);\n --dsw-alias-state-business-tertiary: var(--dsw-static-deepseek-100);\n --dsw-alias-state-success-primary: var(--dsw-static-green-500);\n --dsw-alias-state-success-secondary: var(--dsw-static-green-400);\n --dsw-alias-state-success-tertiary: var(--dsw-static-green-100);\n --dsw-alias-state-warn-label: var(--dsw-static-amber-600);\n --dsw-alias-state-warn-primary: var(--dsw-static-amber-500);\n --dsw-alias-state-warn-secondary: var(--dsw-static-amber-400);\n --dsw-alias-state-warn-tertiary: var(--dsw-static-amber-100);\n --dsw-alias-state-error-primary: var(--dsw-static-red-600);\n --dsw-alias-state-error-secondary: var(--dsw-static-red-400);\n --dsw-static-deepseek-300: rgb(183, 200, 254);\n --dsw-static-deepseek-450: rgb(86, 134, 254);\n --dsw-static-blue-450: rgb(77, 147, 248);\n}\nbody[data-ds-dark-theme] {\n color-scheme: dark;\n --dsw-alias-bg-base: var(--dsw-static-neutral-bluish-950);\n --dsw-alias-bg-layer-1: var(--dsw-static-neutral-bluish-875);\n --dsw-alias-bg-layer-2: var(--dsw-static-neutral-bluish-850);\n --dsw-alias-bg-layer-3: var(--dsw-static-neutral-bluish-800);\n --dsw-alias-border-l1: rgba(255, 255, 255, 0.06);\n --dsw-alias-border-l2: rgba(255, 255, 255, 0.12);\n --dsw-alias-fill-hover: #2b3542;\n --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-50);\n --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-300);\n --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-400);\n --dsw-alias-label-caption: var(--dsw-static-neutral-bluish-600);\n --dsw-alias-markdown-code-block: var(--dsw-static-neutral-bluish-900);\n --dsw-alias-markdown-hr: #465365;\n /* DSH \u6DF1\u8272\u4E3B\u9898\u8BED\u4E49\u72B6\u6001\u522B\u540D\u3002 */\n --dsw-alias-state-business-primary: var(--dsw-static-deepseek-400);\n --dsw-alias-state-business-tertiary: var(--dsw-static-deepseek-800);\n --dsw-alias-state-success-primary: var(--dsw-static-green-500);\n --dsw-alias-state-success-secondary: var(--dsw-static-green-400);\n --dsw-alias-state-success-tertiary: var(--dsw-static-green-900);\n --dsw-alias-state-warn-label: var(--dsw-static-amber-600);\n --dsw-alias-state-warn-primary: var(--dsw-static-amber-500);\n --dsw-alias-state-warn-secondary: var(--dsw-static-amber-400);\n --dsw-alias-state-warn-tertiary: var(--dsw-static-amber-900);\n --dsw-alias-state-error-primary: var(--dsw-static-red-400);\n --dsw-alias-state-error-secondary: var(--dsw-static-red-400);\n}\nhtml { min-height: 100%; }\nbody { margin: 0; min-height: 100vh; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; }\n#genui-root { width: min(1200px, calc(100% - 32px)); margin: 0 auto; padding: 32px 0 56px; }\n";
@@ -0,0 +1,33 @@
1
+ import type { LocaleId } from '../i18n/runtime.ts';
2
+ import type { GenuiSpec } from '../spec.ts';
3
+ import type { BlockInteractionState } from '../interaction-store.ts';
4
+ export declare const GENUI_ARTIFACT_FORMAT = "dsh-genui";
5
+ export declare const GENUI_ARTIFACT_VERSION: 1;
6
+ export type GenuiArtifactTheme = 'light' | 'dark';
7
+ export type GenuiStandaloneAsset = 'mermaid' | 'three' | 'echarts-core' | 'echarts-full';
8
+ export interface GenuiArtifactV1 {
9
+ format: typeof GENUI_ARTIFACT_FORMAT;
10
+ version: typeof GENUI_ARTIFACT_VERSION;
11
+ spec: GenuiSpec;
12
+ state?: BlockInteractionState;
13
+ presentation: {
14
+ locale: LocaleId;
15
+ theme: GenuiArtifactTheme;
16
+ };
17
+ }
18
+ export interface GenuiPortabilityReport {
19
+ requiredAssets: GenuiStandaloneAsset[];
20
+ customTypes: string[];
21
+ externalMedia: string[];
22
+ hasModelActions: boolean;
23
+ }
24
+ export type GenuiExportErrorCode = 'unsupported-custom-component' | 'runtime-fetch-failed' | 'asset-fetch-failed' | 'artifact-invalid' | 'download-failed';
25
+ /** 导出流程遇到可供界面识别的错误时使用。 */
26
+ export declare class GenuiExportError extends Error {
27
+ readonly code: GenuiExportErrorCode;
28
+ constructor(code: GenuiExportErrorCode, message: string);
29
+ }
30
+ export interface CreateGenuiArtifactOptions {
31
+ locale?: LocaleId;
32
+ theme?: GenuiArtifactTheme;
33
+ }
@@ -30,6 +30,8 @@ export interface GenuiBlockProps {
30
30
  /** Native hosts own durable state instead of the browser store. Read once per stateKey. */
31
31
  initialState?: BlockInteractionState | undefined;
32
32
  onStateChange?: ((state: BlockInteractionState) => void) | undefined;
33
+ /** Observe the current secret-free durable state without changing persistence ownership. */
34
+ onStateSnapshot?: ((state: BlockInteractionState) => void) | undefined;
33
35
  }
34
36
  /** Per-question metadata registered by grouped radios for local grading. */
35
37
  export interface QuestionMeta {
@@ -0,0 +1,4 @@
1
+ import type { GenuiEChart } from './spec.ts';
2
+ export declare const CORE_PRESETS: ReadonlySet<string>;
3
+ /** 依照 renderer 共用的 preset 规则选择 ECharts 资源。 */
4
+ export declare function echartEngineFor(node: GenuiEChart): 'echarts-core' | 'echarts-full';
@@ -1,3 +1,4 @@
1
+ export { CORE_PRESETS } from './echarts-engine.ts';
1
2
  /** The ECharts instance surface (the subset the component uses). */
2
3
  export interface EChartsInstance {
3
4
  setOption: (opt: unknown, notMerge?: boolean) => void;
@@ -15,6 +16,3 @@ export interface EChartsInstance {
15
16
  export declare function createChart(el: HTMLElement, option: unknown, opts?: {
16
17
  height?: number;
17
18
  }, engine?: 'core' | 'full'): Promise<EChartsInstance>;
18
- /** Presets the core bundle can draw; anything else (or a raw `option`) needs
19
- * the full engine. Kept next to the loader so the split stays in one place. */
20
- export declare const CORE_PRESETS: ReadonlySet<string>;
@@ -6,3 +6,5 @@ export { isRenderableProcess, processGenuiSpec } from './guard.ts';
6
6
  export { setGenuiAssetBase } from './asset-loader.ts';
7
7
  export type { GenuiSpec } from './spec.ts';
8
8
  export type { BlockInteractionState } from './interaction-store.ts';
9
+ export { analyzeGenuiPortability, buildStandaloneHtml, createGenuiArtifact, downloadGenuiArtifactHtml, downloadGenuiArtifactJson, parseGenuiArtifact, serializeGenuiArtifact, } from './artifact/index.ts';
10
+ export type { GenuiArtifactV1, GenuiPortabilityReport } from './artifact/index.ts';
@@ -9,6 +9,12 @@
9
9
  * `block.*`, `err.*`.
10
10
  */
11
11
  export declare const EN: {
12
+ readonly 'artifact.export': "Export";
13
+ readonly 'artifact.exportHtml': "HTML";
14
+ readonly 'artifact.exportJson': "GenUI JSON";
15
+ readonly 'artifact.exportFailed': "Export failed. Try again.";
16
+ readonly 'artifact.unsupportedCustom': "Cannot export HTML because the standalone runtime cannot render custom components: {types}. Export GenUI JSON to preserve these nodes.";
17
+ readonly 'artifact.externalMediaNotice': "External media may not appear when this file is opened offline.";
12
18
  readonly 'panel.badge': "Panel";
13
19
  readonly 'panel.title.default': "GenUI panel";
14
20
  readonly 'panel.title.explore': "GenUI explorer";
@@ -0,0 +1,4 @@
1
+ export { CodeBlock, DiffBlock, JsonTree, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ export type { DiffBlockLabels, JsonTreeLabels } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ export declare const getGenuiComponent: ((type: string) => unknown) | undefined;
4
+ export declare const GenuiActionContext: unknown;
@@ -6,7 +6,7 @@
6
6
  * module load — a language switch must reach a diff or JSON tree that is
7
7
  * already on screen.
8
8
  */
9
- import type { DiffBlockLabels, JsonTreeLabels } from '@deepseek-ai/dsh-client-ui-primitives';
9
+ import type { DiffBlockLabels, JsonTreeLabels } from './primitive-adapter.ts';
10
10
  /** Chrome labels for an inline GenUI diff block, in the active locale. */
11
11
  export declare function diffBlockLabels(): DiffBlockLabels;
12
12
  /** Copy labels for a code block rendered inside a GenUI fence or node. */
@@ -0,0 +1,2 @@
1
+ /** 从启动代码预先载入的全局资源读取引擎。 */
2
+ export declare function loadGenuiAsset<T>(name: 'mermaid' | 'three' | 'echarts-core' | 'echarts-full'): Promise<T>;
@@ -0,0 +1,9 @@
1
+ import type { GenuiArtifactV1 } from '../artifact/types.ts';
2
+ declare global {
3
+ interface Window {
4
+ __GenuiStandalone__?: {
5
+ mount(root: HTMLElement, artifact: GenuiArtifactV1): () => void;
6
+ };
7
+ }
8
+ }
9
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface DiffBlockLabels {
3
+ copy: string;
4
+ copied: string;
5
+ codeLabel: string;
6
+ wrapLabel: string;
7
+ unwrapLabel: string;
8
+ collapseAria: string;
9
+ expandAria: (hidden: number) => string;
10
+ collapse: string;
11
+ expand: (hidden: number) => string;
12
+ files: (count: number) => string;
13
+ }
14
+ export interface JsonTreeLabels {
15
+ copyValue: string;
16
+ copyJson: string;
17
+ copyPath: string;
18
+ copyPrettyJson: string;
19
+ copyCompactJson: string;
20
+ copied: string;
21
+ copyFailed: string;
22
+ collapseNode: string;
23
+ expandNode: string;
24
+ copyButtonTitle: (action: string) => string;
25
+ }
26
+ /** 在独立页面显示可复制的代码内容。 */
27
+ export declare function CodeBlock({ code, lang, copyLabel, copiedLabel }: {
28
+ code: string;
29
+ lang?: string;
30
+ copyLabel?: string;
31
+ copiedLabel?: string;
32
+ }): ReactNode;
33
+ /** 在独立页面以差异文本显示文件变更。 */
34
+ export declare function DiffBlock({ diffs }: {
35
+ diffs: Array<{
36
+ path: string;
37
+ oldText?: string;
38
+ newText?: string;
39
+ }>;
40
+ labels?: DiffBlockLabels;
41
+ }): ReactNode;
42
+ /** 在独立页面显示 JSON,并提供本地折叠控制。 */
43
+ export declare function JsonTree({ data, label, copyable, labels }: {
44
+ data: object | unknown[];
45
+ label?: string;
46
+ copyable?: boolean;
47
+ labels?: JsonTreeLabels;
48
+ }): ReactNode;
49
+ /** 将文本复制到系统剪贴板。 */
50
+ export declare function writeClipboard(text: string): Promise<boolean>;
51
+ /** standalone 模式不连接 DSH 注册表,因此自定义组件始终不可用。 */
52
+ export declare function getGenuiComponent(_type: string): undefined;
53
+ /** standalone 模式专用的空动作上下文。 */
54
+ export declare const GenuiActionContext: import("react").Context<((action: string, payload: Record<string, unknown>) => void) | undefined>;
@@ -0,0 +1,2 @@
1
+ import './katex-style.ts';
2
+ import './bootstrap-types.ts';
@@ -26,6 +26,8 @@ import type { Context } from '@deepseek-ai/cordis';
26
26
  import type { UserMessage } from '@deepseek-ai/dsh-session';
27
27
  /** Plugin name recorded on every message this loop steers. */
28
28
  export declare const FEEDBACK_PLUGIN_NAME = "@changfenhuang/dsh-genui";
29
+ /** Source kind persisted by this plugin in Session format v4. */
30
+ export declare const FEEDBACK_SOURCE_KIND: "plugin:@changfenhuang/dsh-genui";
29
31
  /** One ```dsh-ui fence found in an assistant reply. */
30
32
  export interface ExtractedFence {
31
33
  /** Raw body between the fences (no delimiters). */
@@ -74,9 +76,10 @@ export declare function fenceCorrectionText(failures: readonly FenceFailure[]):
74
76
  * copy resolves identically on every host.
75
77
  *
76
78
  * @param text - the correction text.
79
+ * @param sessionFormatVersion - the format recorded by the active session.
77
80
  * @returns a frozen user message attributed to this plugin as a notice.
78
81
  */
79
- export declare function createFeedbackMessage(text: string): UserMessage;
82
+ export declare function createFeedbackMessage(text: string, sessionFormatVersion: number): UserMessage;
80
83
  /** What the pure planner needs to decide whether a correction may be sent. */
81
84
  export interface FenceFeedbackPlanInput {
82
85
  readonly text: string;