Skip to content

fix: Optimize AI generation workflows - #7924

Merged
shaohuzhang1 merged 1 commit into
v3from
pr@v3@fix_ai_gen
Nov 13, 2026
Merged

shaohuzhang1 merged 1 commit into
v3from
pr@v3@fix_ai_gen

Conversation

@shaohuzhang1

Copy link
Copy Markdown
Contributor

fix: Optimize AI generation workflows

config,
returns,
useAgent,
} satisfies NodeAgentModule

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my review of the diff:

Bugs / Correctness Issues

  1. api_input_field_list keyed by field.variable — but argReference expects a string name. String(field.variable) is used as the lookup key, yet variable is typed unknown. If variable is actually a reference object (array) rather than a plain name, String(...) will yield garbage like "[object Object]" or a comma-joined list. Verify the actual shape; the type should be tightened (e.g., variable: string) and validated.

  2. Silent skipping of missing optional params retains stale references. When reference is undefined for an optional field, you keep field unchanged — including any previously-set value. On re-invocation this means old bindings persist unexpectedly. Consider explicitly clearing value ({ ...field, value: [] }) when no reference is provided.

  3. Required-field check depends on argReference returning falsy for both "not provided" and "provided but invalid". A malformed reference could be treated as "missing", masking the real error. Distinguish "absent" from "invalid".

  4. additionalProperties on params conflicts with explicit question property semantics. The additional-properties schema describes "the remaining inputs (api / form)", but nothing validates that extra keys correspond to actual inputs[].name from add_node. Unknown keys are silently ignored in the apply. Either validate them or reject unknown keys.

  5. question is required in params, but apply treats it as optional (if (question) { … }). Either enforce it in apply (return an error when missing) or drop it from required. Inconsistent contract.

  6. is_result fallback logic is confusing:

    is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData as { is_result?: boolean }).is_result ?? false,

    ?? false overrides the documented "auto-true when no downstream node" behavior — this default should presumably be decided by validation/runtime, not hardcoded here. Also (nodeData as {...}).is_result should just be part of the readNodeData generic type.

Type-Safety Issues

  1. Double cast hackery. apiInputFieldList as ApiInputField[] / userInputFieldList as UserInputField[] after building with ErrorResult union elements hides a design smell. Better: collect errors separately or use for..of with early return, so the array is always cleanly typed without casting.

  2. variable: unknown should be a concrete type. unknown prevents proper handling.

  3. returns: { result: { type: 'any' } } — verify 'any' is a supported JSON-schema type in your validator; JSON Schema doesn't have 'any'.

  4. JsonObjectSchema reused for config/params where additionalProperties has a schema — ensure your schema type allows a schema object (not just boolean) for additionalProperties; otherwise the params definition isn't valid per the declared type.

Style / Structure

  1. Repeated map/find-pattern is duplicated for api_input_field_list and user_input_field_list. Extract a helper:

    function bindFields<T extends { is_required: boolean; required: boolean }>(/* … */)

    or a small generic mapWithErrors(list, fn).

  2. ...nodeData spread then explicit overrides also carries through any unrelated fields — fine, but combined with build(deps): bump the pip group across 1 directory with 1 update #2 (stale values) it's a place to audit.

  3. No handling for empty application_id/missing published agent at config time (description mentions it's set during add_node, but apply never checks). If add_node failed to populate these lists, the node silently saves with no bindings.

  4. Comment header mixes Chinese and code conventions — fine if project-wide, but the JSDoc-style block for a non-JSDoc target is slightly inconsistent; verify lint rules.

Minor

  1. argReference(context, String(field.variable)) vs argReference(context, field.field) — inconsistent key extraction; make both go through a single accessor.
  2. Consider logging/returning which params were left unbound (non-required) for better debuggability.

Suggested Priority

@shaohuzhang1
shaohuzhang1 merged commit e73f804 into v3 Nov 13, 2026
3 of 4 checks passed
@shaohuzhang1
shaohuzhang1 deleted the pr@v3@fix_ai_gen branch November 13, 2026 11:40
config,
returns,
useAgent,
} satisfies NodeAgentModule

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: tool-workflow-lib-node AI generation config

Potential Issues / Irregularities

1. additionalProperties: { type: 'any' } in params

  • type: 'any' is not valid JSON Schema. The standard way to express arbitrary values is an empty schema {} or omitting type. Depending on the schema validator used, type: 'any' may cause validation errors or be silently ignored (which could reject valid inputs).
  • Also, additionalProperties applies to keys not matched by properties. Since properties is empty, this is effectively fine — but confirm the consumer treats it as "any key allowed" rather than enforcing a fixed schema.

2. returns schema is empty/misleading

  • { type: 'object', properties: {}, additionalProperties: true } doesn't actually describe the dynamic output fields. If downstream agents rely on returns to know field names/types, they get nothing useful. Consider documenting (in description) that fields are dynamic, or populating returns from the node's declared output params at runtime.

3. Return value of apply when node missing

if (!node) return new ErrorResult(...)
  • apply returns ErrorResult here and finishWithValidate(context) elsewhere. Verify the union return type is compatible with the expected signature. Inconsistent return shapes (one throws/errors, one finishes) are fine only if the framework handles both. Worth confirming finishWithValidate returns the same type.

4. Mutating values when source === 'custom'

  • When passing value straight through, there's no validation against field.type or field.is_required beyond undefined checks. A required field given null or '' passes. Consider validating against declared type (e.g., string vs number) — or at least rejecting null for required fields.

5. Reference staleness can silently persist

if (reference) { ... continue }
if (field.source === 'reference') { inputFields.push(field); continue }
  • The comment says "续聊未重新绑定" keeps old reference. That's intentional, but note: a reference cannot be cleared via this tool. If a user wants to switch a previously-referenced param back to a constant, there's no path unless argReference returns falsy AND source flips — which it won't, because source === 'reference' keeps it. This is a functional gap worth documenting or addressing (e.g., allow values[field] = ... even when currently reference).

6. is_result fallback logic

is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? false)
  • Fine, but nodeData.is_result ?? false — if is_result was previously set to a non-boolean truthy value, it would pass through. Low risk given typed access but consider coercing.

7. Spread + mutation pattern

  • nodeData is read via readNodeData<...>, then spread into node_data again in writeNodeProperties. This copies everything, including possibly stale/extra fields you don't intend to persist. Generally OK, but confirm there aren't unmarshalled/prototype keys (use plain object if so). Also input_fields ?? [] — good defensive default.

8. Missing guard when input_field_list is absent

  • If a new/partially-created node has no input_field_list, the loop does nothing and writeNodeProperties overwrites with input_field_list: []. This could silently clear or block required inputs the signature expects. Consider erroring if input_field_list is missing (mirrors the "signature comes from add_node" assumption).

9. argReference semantics in loop

  • argReference(context, field.field) is called per field; ensure it's cheap and side-effect free (fresh array each call). The [...reference] copy is good defensive practice.

Optimization Suggestions

  1. Precompute arg references once outside the loop instead of calling per-field, if argReference is non-trivial.
  2. Determine validity without writes: if any required field is unfilled/undefined AND not already referenced, return early before building the full list — avoids partial work (though you already return below, so minor).
  3. Add type hints in descriptions for values — e.g., "值类型应匹配参数声明的 type".
  4. Return richer error info: current ErrorResult messages are strings; if supported, include structured field-level info ({ field, reason }) for better agent self-correction.
  5. Consider idempotency note in prompt: re-calling update merges, doesn't replace — the prompt mentions "只传要设置的字段,其余保持不变", which aligns well. Keep as-is.
  6. Consolidate prompt tips: the - 必填参数都要有值 line duplicates behavior enforced by code returning ErrorResult; still useful for agent guidance, keep.

Minor / Style

  • Long template literal for the required-field error — consider a helper for readability.
  • Record<string, unknown> & { input_field_list?: WorkflowToolInputField[] } verbose; could define a named interface WorkflowToolNodeData.
  • Comment density and clarity are good.

Summary

Most impactful concerns:

})

defineExpose({ open, close })
</script>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my review of the code:

Potential Issues

  1. No wrapRef type guarantee — scrollbarRef.value?.wrapRef relies on an internal/undocumented property of whatever scrollbar component this is. Verify that wrapRef is a supported public API; if not, it may break on library upgrades.

  2. Missing cleanup on error paths — bottomScroll.destroy() only happens in onBeforeUnmount. If the component is kept alive with keep-alive, the instance may leak or the element reference may go stale. Consider using onDeactivated.

  3. followBottom swallows subsequent calls without rescheduling awareness — When followPending is true and a new event arrives, the request is dropped. That's the intended debounce, but note: since nextTick callbacks run after the current tick, multiple events in one tick collapse into a single scroll — good. However, if new content is appended after the nextTick fires (e.g., async render), the follow won't re-trigger. This can cause the view to end slightly above the true bottom during streaming. Consider looping while scrollTop < threshold after render, or reserving a small offset.

  4. Race between forceBottom and followBottom — If a user clicks "send" (forceBottom) while followPending is already true from a prior followBottom, the pending scrollBottom() will still fire afterwards. Not necessarily wrong, but worth verifying ordering semantics with your Scroll class.

  5. getBottomScroll() called inside nextTick — Fine, but the element check happens at callback time; if the drawer unmounts during that tick, wrapRef may be null (handled) but bottomScrollElement stays pointing to a detached node until next call. Add bottomScroll = undefined; bottomScrollElement = undefined after destroy for clarity.

  6. Type mismatch comment — The comments are in Chinese while the surrounding code style/project conventions may require English or i18n. Minor, but check project standards.

Optimization Suggestions

  1. Debounce via requestAnimationFrame instead of nextTick+boolean flag if you want smoother throttling during rapid streaming:

    let rafId = 0
    function followBottom() {
      if (rafId) return
      rafId = requestAnimationFrame(() => {
        rafId = 0
        getBottomScroll()?.scrollBottom()
      })
    }

    Be sure to cancelAnimationFrame(rafId) on unmount.

  2. Reset state on destroy:

    onBeforeUnmount(() => {
      agent?.stop()
      bottomScroll?.destroy()
      bottomScroll = undefined
      bottomScrollElement = undefined
    })
  3. Guard against concurrent new Scroll(wrap) — If getBottomScroll is called while a previous instance exists but was never destroyed due to some path, ensure idempotency. Current code handles element change correctly, but consider also checking bottomScroll truthiness explicitly.

  4. Minimize allocations — Only instantiate Scroll when actually needed. Currently a Scroll is created on first followBottom even if the user never needs following. Low impact, but could be lazy-initialised in the actual scroll method.

  5. Observability — Since this replaces simple setScrollTop(Number.MAX_SAFE_INTEGER), add a small guard so that if Scroll fails to construct (throws), the code falls back to the old behavior:

    try { bottomScroll = new Scroll(wrap) } catch { bottomScroll = undefined }
  6. Consider a threshold constant — The "bottom附近恢复跟随" logic presumably lives in Scroll. Confirm the threshold is configurable and matches UX requirements for the drawer height.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant