Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions docs/developers/qwen-serve-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -945,15 +945,19 @@ presence metadata, startup state, and runtime state; literal secrets are never
returned. Channel snapshots use `Cache-Control: no-store`.

Field descriptors can expose nested object metadata through `properties`.
Numeric descriptors can use `exclusiveMinimum` for open lower bounds. Clients
that do not render an advertised field kind must preserve its existing config
value instead of coercing or deleting it. Object fields cannot be required,
and nested properties cannot be secrets or environment-resolvable fields;
those management protocols remain top-level only. A nested `required` property
is enforced only while its parent object is present in the write; omitting the
parent object leaves its nested requirements unchecked. Writes replace each
field's stored value wholesale, so preserving an object means resending the
stored object; the daemon does not merge partial objects.
Numeric descriptors can use `exclusiveMinimum` for open lower bounds. String
and secret descriptors can use `multiline` to ask clients for a multi-line text
area; the descriptor types allow it only on top-level fields. Clients that do
not render an advertised field kind must preserve its existing config value
instead of coercing or deleting it, and a client that renders a `multiline`
field in a single-line control must preserve the stored value verbatim instead
of writing back its newline-stripped input value. Object fields cannot be
required, and nested properties cannot be secrets or environment-resolvable
fields; those management protocols remain top-level only. A nested `required`
property is enforced only while its parent object is present in the write;
omitting the parent object leaves its nested requirements unchecked. Writes
replace each field's stored value wholesale, so preserving an object means
resending the stored object; the daemon does not merge partial objects.

Configuration writes use optimistic concurrency and the strict operator-authority
gate:
Expand Down
12 changes: 11 additions & 1 deletion packages/channels/base/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ export interface ChannelConfigValueFieldDescriptor
kind: 'string' | 'secret';
required?: boolean;
envResolvable?: boolean;
/** Render the field as a multi-line text area in management UIs. */
multiline?: boolean;
properties?: never;
}

Expand All @@ -462,6 +464,7 @@ export interface ChannelConfigPlainValueFieldDescriptor
kind: 'boolean' | 'string-list' | 'record';
required?: boolean;
envResolvable?: never;
multiline?: never;
properties?: never;
}

Expand All @@ -470,6 +473,7 @@ export interface ChannelConfigEnumFieldDescriptor
kind: 'enum';
required?: boolean;
envResolvable?: never;
multiline?: never;
options: ReadonlyArray<{ value: string; label: string }>;
properties?: never;
}
Expand All @@ -479,6 +483,7 @@ export interface ChannelConfigNumberFieldDescriptor
kind: 'number';
required?: boolean;
envResolvable?: never;
multiline?: never;
exclusiveMinimum?: number;
properties?: never;
}
Expand All @@ -488,16 +493,21 @@ export interface ChannelConfigObjectFieldDescriptor
kind: 'object';
required?: false;
envResolvable?: never;
multiline?: never;
properties: readonly ChannelConfigNestedFieldDescriptor[];
}

export type ChannelConfigNestedFieldDescriptor =
| (Omit<ChannelConfigValueFieldDescriptor, 'kind' | 'envResolvable'> & {
| (Omit<
ChannelConfigValueFieldDescriptor,
'kind' | 'envResolvable' | 'multiline'
> & {
kind: Exclude<
ChannelConfigFieldKind,
'secret' | 'enum' | 'number' | 'object'
>;
envResolvable?: never;
multiline?: never;
})
| (Omit<ChannelConfigEnumFieldDescriptor, 'kind' | 'envResolvable'> & {
kind: 'enum';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ function assertDescriptorWireShape(
(descriptor.kind === 'string' || descriptor.kind === 'secret')
) {
allowedKeys.add('envResolvable');
allowedKeys.add('multiline');
}
if (descriptor.kind === 'number') {
allowedKeys.add('exclusiveMinimum');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,22 @@ describe('built-in channel registry', () => {
'groupPolicy',
'sessionScope',
'multiSession',
'instructions',
]);
expect(
entry?.fields.find((field) => field.key === 'senderPolicy'),
).toMatchObject({ default: 'pairing' });
// The shared descriptor is injected into every manageable channel, and
// dingtalk substitutes its own default block instead of composing with it
// (DingtalkAdapter.ts:908), so pin both the multiline render hint and the
// neutral copy: neither survives an accidental revert to a plain string
// field or to text that promises additive guidance.
const instructions = entry?.fields.find(
(field) => field.key === 'instructions',
);
expect(instructions).toMatchObject({ kind: 'string', multiline: true });
Comment thread
yiliang114 marked this conversation as resolved.
expect(instructions?.description).toContain(
'replace their own default guidance',
);
});
});
29 changes: 29 additions & 0 deletions packages/cli/src/commands/channel/channel-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,35 @@ describe('channel registry', () => {
.filter((entry) => entry.manageable)
.map((entry) => entry.type),
).toEqual(['dingtalk', 'dws', 'wecom', 'feishu', 'github', 'gitlab']);
// The registry skips the shared `instructions` injection for any channel
// that declares its own, so pin the render invariants the editor depends on
// for every manageable built-in: exactly one field, plus the multiline hint
// (without it the editor falls back to a single-line input that flattens
// stored guidance on the first edit). The copy guarantee is scoped to the
// injected descriptor, because a channel declaring its own `instructions`
// takes the skip branch and may carry tailored neutral copy, and it is
// asserted where an operator actually reads it
// (ChannelEditorDialog.test.tsx): fieldDescription resolves
// `${labelKey}.description` and falls back to this literal only when that
// i18n key is missing.
for (const entry of builtinCatalog.filter((item) => item.manageable)) {
const instructions = entry.fields.filter(
(field) => field.key === 'instructions',
);
expect(instructions).toHaveLength(1);
expect(instructions[0]).toMatchObject({
kind: 'string',
multiline: true,
});
const declaresOwnInstructions = (
await getPlugin(entry.type)
)?.management?.fields?.some((field) => field.key === 'instructions');
if (!declaresOwnInstructions) {
expect(instructions[0].description).toContain(
'replace their own default guidance',
);
}
}
expect(
catalog.find((entry) => entry.type === 'dingtalk')?.fields,
).toContainEqual(
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/commands/channel/channel-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ function managementFieldsWithSharedControls(
'Retain an owner-scoped catalog of named tasks in daemon-managed mode',
},
]),
...(declared.has('instructions')
? []
: [
{
key: 'instructions',
label: 'Instructions',
kind: 'string' as const,
multiline: true,
Comment thread
yiliang114 marked this conversation as resolved.
description:
'Guidance injected into each channel session context; some channels replace their own default guidance when this is set',
},
]),
];
}

Expand Down
12 changes: 11 additions & 1 deletion packages/sdk-typescript/src/daemon/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4003,6 +4003,8 @@ export interface DaemonChannelConfigValueFieldDescriptor
kind: 'string' | 'secret';
required?: boolean;
envResolvable?: boolean;
/** Render the field as a multi-line text area in management UIs. */
multiline?: boolean;
Comment thread
yiliang114 marked this conversation as resolved.
properties?: never;
}

Expand All @@ -4011,6 +4013,7 @@ export interface DaemonChannelConfigPlainValueFieldDescriptor
kind: 'boolean' | 'string-list' | 'record';
required?: boolean;
envResolvable?: never;
multiline?: never;
properties?: never;
}

Expand All @@ -4019,6 +4022,7 @@ export interface DaemonChannelConfigEnumFieldDescriptor
kind: 'enum';
required?: boolean;
envResolvable?: never;
multiline?: never;
options: ReadonlyArray<{ value: string; label: string }>;
properties?: never;
}
Expand All @@ -4028,6 +4032,7 @@ export interface DaemonChannelConfigNumberFieldDescriptor
kind: 'number';
required?: boolean;
envResolvable?: never;
multiline?: never;
exclusiveMinimum?: number;
properties?: never;
}
Expand All @@ -4037,16 +4042,21 @@ export interface DaemonChannelConfigObjectFieldDescriptor
kind: 'object';
required?: false;
envResolvable?: never;
multiline?: never;
properties: readonly DaemonChannelConfigNestedFieldDescriptor[];
}

export type DaemonChannelConfigNestedFieldDescriptor =
| (Omit<DaemonChannelConfigValueFieldDescriptor, 'kind' | 'envResolvable'> & {
| (Omit<
DaemonChannelConfigValueFieldDescriptor,
'kind' | 'envResolvable' | 'multiline'
> & {
kind: Exclude<
DaemonChannelConfigFieldKind,
'secret' | 'enum' | 'number' | 'object'
>;
envResolvable?: never;
multiline?: never;
})
| (Omit<DaemonChannelConfigEnumFieldDescriptor, 'kind' | 'envResolvable'> & {
kind: 'enum';
Expand Down
Loading
Loading