Skip to content
English

Schema 字段核验与明确预览范围 ​

策略的具体实现如下。

ts
export function createReplyPreviewRules(options: {
  readonly stages: readonly ReplyPreviewStage[];
  readonly tools: readonly ReplyPreviewTool[];
}): readonly ReplyPreviewRule[] {
  const tools = new Map<string, ReplyPreviewTool>();
  for (const capability of options.tools) {
    const id = capability.id;
    breakWhile(!id || tools.has(id), 'Duplicate preview capability declaration');
    const preview = previewSnapshot(capability.preview);
    // Validate and emit the same canonical snapshot, never reread App getters.
    const jsonSchema = preview ? structuredClone(capability.jsonSchema) : {};
    tools.set(id, { id, preview, jsonSchema });
  }
  const stages = new Set<string>(),
    rules: ReplyPreviewRule[] = [];
  for (const stage of options.stages) {
    const { id: stageId, preview, toolIds } = stage;
    breakWhile(!stageId || stages.has(stageId), 'Duplicate preview stage declaration');
    stages.add(stageId);
    const ids = toolIds === undefined ? [] : stringList(toolIds, 'capability IDs');
    breakWhile(
      new Set(ids).size !== ids.length || ids.some((id) => !tools.has(id)),
      'Preview stage references an unknown capability',
    );
    breakWhile(
      preview !== undefined && !['text', 'tools'].includes(preview),
      'Unknown preview stage mode',
    );
    if (!preview) continue;
    if (preview === 'text') {
      breakWhile(ids.length > 0, 'Text preview requires a declared plain-text stage');
      rules.push(Object.freeze({ stageId }));
      continue;
    }
    for (const id of ids) {
      const capability = tools.get(id)!;
      if (!capability.preview) continue;
      const source = capability.preview === true ? {} : capability.preview;
      const { paths, when } = source;
      const checkedPaths =
        paths ??
        [['text'], ['bubbles', '*']].filter((path) => fieldExists(capability.jsonSchema, path));
      breakWhile(
        !checkedPaths.length ||
          checkedPaths.some((path) => !fieldExists(capability.jsonSchema, path)),
        `Invalid preview field: ${id}`,
      );
      breakWhile(
        !!when && !fieldExists(capability.jsonSchema, when.path),
        `Invalid preview eligibility field: ${id}`,
      );
      // Only generator-owned snapshots are frozen; App declarations remain untouched.
      const emittedPaths = Object.freeze(checkedPaths.map((path) => Object.freeze(path)));
      const emittedWhen =
        when &&
        Object.freeze({ path: Object.freeze(when.path), values: Object.freeze(when.values) });
      rules.push(
        Object.freeze({
          stageId,
          toolId: wireName(id),
          paths: emittedPaths,
          ...(emittedWhen ? { when: emittedWhen } : {}),
        }),
      );
    }
  }
  return Object.freeze(rules);
}

function fieldExists(root: JsonSchema, path: readonly string[]): boolean {
  const visit = (schema: unknown, remaining: readonly string[], depth: number): boolean => {
    if (!schema || typeof schema !== 'object' || depth > 64) return false;
    if (!remaining.length) return true;
    const current = schema as Record<string, unknown>;
    if (typeof current.$ref === 'string' && current.$ref.startsWith('#/')) {
      const target = current.$ref
        .slice(2)
        .split('/')
        .reduce<unknown>(
          (value, part) =>
            value && typeof value === 'object'
              ? (() => {
                  const key = part.replace(/~1/g, '/').replace(/~0/g, '~');
                  return Object.hasOwn(value, key)
                    ? (value as Record<string, unknown>)[key]
                    : undefined;
                })()
              : undefined,
          root,
        );
      if (visit(target, remaining, depth + 1)) return true;
    }
    for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
      const alternatives = current[keyword];
      if (
        Array.isArray(alternatives) &&
        alternatives.some((value) => visit(value, remaining, depth + 1))
      )
        return true;
    }
    const [key, ...rest] = remaining;
    const properties = current.properties;
    if (
      key !== '*' &&
      properties &&
      typeof properties === 'object' &&
      Object.hasOwn(properties, key!) &&
      visit((properties as Record<string, unknown>)[key!], rest, depth + 1)
    )
      return true;
    return (key === '*' || /^\d+$/.test(key!)) && visit(current.items, rest, depth + 1);
  };
  return visit(root, path, 0);
}

接口参考、使用示例与开发指南。