dsh-codinput 0.3.5 → 0.3.6
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/CHANGELOG.md +19 -0
- package/README.md +4 -0
- package/TESTING.md +3 -0
- package/lib/client.js +28 -28
- package/package.json +1 -1
- package/src/client/components/InputSlotSeat.tsx +3 -34
- package/src/client/components/ModelSlot.tsx +83 -0
- package/src/client/components/Toolbar.tsx +8 -2
- package/src/client/components/slotEntry.tsx +140 -26
- package/src/client/host-types.ts +8 -0
package/package.json
CHANGED
|
@@ -25,9 +25,8 @@
|
|
|
25
25
|
* 私有),现网无此类条目;如出现则降级为无 store props 渲染。
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { EntryBoundary, entryId, entryInjectProps, listRows, translatorFor, useSlotEntries } from './slotEntry';
|
|
28
|
+
import { useMemo } from 'react';
|
|
29
|
+
import { entryId, listRows, renderSlotEntry, useSlotEntries } from './slotEntry';
|
|
31
30
|
|
|
32
31
|
export interface InputSlotSeatProps {
|
|
33
32
|
/** 官方子槽位全名(如 conversation.input.left)。 */
|
|
@@ -46,35 +45,5 @@ export function InputSlotSeat(props: InputSlotSeatProps): JSX.Element | null {
|
|
|
46
45
|
// 官方同款门槛:无会话不渲染(input.left / input.right 都是 strict session 槽位)。
|
|
47
46
|
if (sessionId === undefined || rows.length === 0) return null;
|
|
48
47
|
|
|
49
|
-
return (
|
|
50
|
-
<>
|
|
51
|
-
{rows.map((entry, index) => {
|
|
52
|
-
const Comp = entry.component as React.ComponentType<Record<string, unknown>>;
|
|
53
|
-
// 条目词典的 t(官方 localeSeat 同款);无 locale 或绑定失败 → 不下发,
|
|
54
|
-
// 组件按各自回退(enhancer 回退内置中文词典)。
|
|
55
|
-
let t: unknown;
|
|
56
|
-
try {
|
|
57
|
-
t = translatorFor(entry) ?? undefined;
|
|
58
|
-
} catch {
|
|
59
|
-
t = undefined;
|
|
60
|
-
}
|
|
61
|
-
let injected: Record<string, unknown> | null = null;
|
|
62
|
-
try {
|
|
63
|
-
injected = entryInjectProps(entry, sessionId);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
diagError('slot.seat.inject', error);
|
|
66
|
-
}
|
|
67
|
-
return createElement(
|
|
68
|
-
EntryBoundary,
|
|
69
|
-
{ key: entryId(entry, index) },
|
|
70
|
-
createElement(Comp, {
|
|
71
|
-
...(standard ?? {}),
|
|
72
|
-
sessionId,
|
|
73
|
-
...(injected ?? {}),
|
|
74
|
-
...(t !== undefined ? { t } : {}),
|
|
75
|
-
}),
|
|
76
|
-
);
|
|
77
|
-
})}
|
|
78
|
-
</>
|
|
79
|
-
);
|
|
48
|
+
return <>{rows.map((entry, index) => renderSlotEntry(entry, entryId(entry, index), { sessionId, standard }))}</>;
|
|
80
49
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型座单值镜像(issue #4):`conversation.input.model`。
|
|
3
|
+
*
|
|
4
|
+
* 座位事实(宿主 0.2.1-rc.2 取证):
|
|
5
|
+
* - `conversation.input.model` 是 `{kind:"single", scope:"session"}` 单值槽,
|
|
6
|
+
* 由官方 InputBar 条目(`conversation.composer.bar` 缺省条目)声明,唯一
|
|
7
|
+
* 渲染点在 InputBar 体内:`input.model → renderSlot(..., { locked })`;
|
|
8
|
+
* 官方占用者是 client-ui-model-selection 的 ModelSelect(替换风险标记
|
|
9
|
+
* `shadows-shipped-ui`,生态替换类插件按契约以 priority 遮蔽它)。
|
|
10
|
+
* - Codinput 以 priority 遮蔽整个 bar 后,该 renderSlot 调用点随官方条目一起
|
|
11
|
+
* 消失;但「遮蔽」不是 dispose——宿主条目仍在 ledger 上,座位声明仍在,
|
|
12
|
+
* 第三方对它的 `slots.inject` 照常触发、`register` 照常成功(单值槽低
|
|
13
|
+
* priority 无人竞争时静默赢)、`entries` 里它确实是赢家,只是没有任何
|
|
14
|
+
* renderSlot 会读它。表现为「插件装了、不报错、永不上屏」。
|
|
15
|
+
*
|
|
16
|
+
* 本组件的收敛口径(0.3.6 起):
|
|
17
|
+
* - **只镜像第三方遮蔽者**(宿主动态包:register 被强制分配负数遮蔽秩、
|
|
18
|
+
* fiber 名带 `dyn/` 前缀),见 `isShadowingOccupant`;
|
|
19
|
+
* - **出厂占用的 ModelSelect 不镜像**——那个座位一直由 Codinput 自绘的模型
|
|
20
|
+
* 芯片代表(模型名显示仍走官方 `conversation.input.model.label` 子座),
|
|
21
|
+
* 镜像出厂条目会与自绘芯片重复成两个模型选择器;
|
|
22
|
+
* - 有第三方遮蔽者时**让位**:镜像其组件、隐藏自绘芯片(同座只留一个控件,
|
|
23
|
+
* 与官方「一个单值座只画一个占用者」一致)。
|
|
24
|
+
*
|
|
25
|
+
* 官方门槛同款复刻:`sessionId === undefined` 时该座位不渲染(此时自绘芯片
|
|
26
|
+
* 保持现状)。owner prop `locked`(官方 `InputControlOwnerProps`:会话是否
|
|
27
|
+
* 拒绝交互)原样下发——生态替换类插件按契约消费它(如 reasoning-slider 的
|
|
28
|
+
* `props.locked === true` 置灰)。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useMemo } from 'react';
|
|
32
|
+
import type { ReactNode } from 'react';
|
|
33
|
+
import type { SlotEntryLike } from '../host-types';
|
|
34
|
+
import {
|
|
35
|
+
entryId,
|
|
36
|
+
entryInjectFailed,
|
|
37
|
+
firstComponentEntry,
|
|
38
|
+
isShadowingOccupant,
|
|
39
|
+
renderSlotEntry,
|
|
40
|
+
useSlotEntries,
|
|
41
|
+
} from './slotEntry';
|
|
42
|
+
|
|
43
|
+
const MODEL_SLOT = 'conversation.input.model';
|
|
44
|
+
|
|
45
|
+
export interface ModelSlotProps {
|
|
46
|
+
sessionId: string | undefined;
|
|
47
|
+
/** 宿主下发给本插件条目的 standard props 原样面(见 InputSlotSeat)。 */
|
|
48
|
+
standard: Record<string, unknown> | undefined;
|
|
49
|
+
/** 官方 owner prop locked(会话拒绝交互时第三方座条目应置灰)。 */
|
|
50
|
+
locked: boolean;
|
|
51
|
+
/** 无第三方遮蔽者时的自绘模型芯片(保持既有 UX)。 */
|
|
52
|
+
fallback: ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 模型座的第三方遮蔽者(无则 null):单值槽赢家且命中动态包判据。 */
|
|
56
|
+
export function useModelSeatOccupant(): SlotEntryLike | null {
|
|
57
|
+
const entries = useSlotEntries(MODEL_SLOT);
|
|
58
|
+
return useMemo(() => {
|
|
59
|
+
const winner = firstComponentEntry(entries);
|
|
60
|
+
return winner !== null && isShadowingOccupant(winner) ? winner : null;
|
|
61
|
+
}, [entries]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ModelSlot(props: ModelSlotProps): JSX.Element | null {
|
|
65
|
+
const { sessionId, standard, locked, fallback } = props;
|
|
66
|
+
const occupant = useModelSeatOccupant();
|
|
67
|
+
|
|
68
|
+
// 官方门槛:无会话不画该座位;自绘芯片(fallback)保持既有行为。
|
|
69
|
+
if (sessionId === undefined || occupant === null) return <>{fallback}</>;
|
|
70
|
+
|
|
71
|
+
// 让位可行性:inject 面必须在宿主 fiber 上下文里才算数(见 slotEntry)。
|
|
72
|
+
// 若该条目的 inject 依赖宿主服务面(插件侧调用会抛 without inject),
|
|
73
|
+
// 让位降级——回退自绘芯片(它本就是官方模型选择的等价物),而不是留空。
|
|
74
|
+
if (entryInjectFailed(occupant, sessionId)) return <>{fallback}</>;
|
|
75
|
+
|
|
76
|
+
return renderSlotEntry(occupant, `model:${entryId(occupant, 0)}`, {
|
|
77
|
+
sessionId,
|
|
78
|
+
standard,
|
|
79
|
+
ownerProps: { locked },
|
|
80
|
+
// 组件本体崩溃同样回退自绘芯片(逐条目边界已在 renderSlotEntry 内)。
|
|
81
|
+
fallback,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -18,6 +18,7 @@ import { openCommandMenu } from '../triggers';
|
|
|
18
18
|
import { t, useT } from '../i18n';
|
|
19
19
|
import { IconPlusOfficial, IconSendOfficial, IconStopOfficial } from '../icons';
|
|
20
20
|
import { ModelMenu } from './ModelMenu';
|
|
21
|
+
import { ModelSlot } from './ModelSlot';
|
|
21
22
|
import { PermissionMenu } from './PermissionMenu';
|
|
22
23
|
import { VoiceActivity } from './VoiceActivity';
|
|
23
24
|
import { InputLeftSlot } from './InputLeftSlot';
|
|
@@ -105,9 +106,14 @@ export function Toolbar(props: ToolbarProps): JSX.Element {
|
|
|
105
106
|
{state !== undefined && sessionId !== undefined ? (
|
|
106
107
|
<InputRightSlot sessionId={sessionId} standard={inputStandard} />
|
|
107
108
|
) : null}
|
|
108
|
-
|
|
109
|
+
{/* 官方右区顺序:input.right → input.model。模型座单值槽的第三方
|
|
110
|
+
遮蔽者(如 reasoning-slider / dsh-model-accordion / effort-dial)
|
|
111
|
+
由 ModelSlot 镜像并让位;无遮蔽者时回退自绘模型芯片(issue #4)。 */}
|
|
112
|
+
<ModelSlot
|
|
109
113
|
sessionId={sessionId}
|
|
110
|
-
|
|
114
|
+
standard={inputStandard}
|
|
115
|
+
locked={disabled === true}
|
|
116
|
+
fallback={<ModelMenu sessionId={sessionId} onNotify={onNotify} />}
|
|
111
117
|
/>
|
|
112
118
|
</div>
|
|
113
119
|
<div className="dci-voice-seat">
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 共用同一套合成公式,抽到这里避免多份漂移)。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useCallback, useSyncExternalStore, Component } from 'react';
|
|
8
|
+
import { createElement, useCallback, useSyncExternalStore, Component } from 'react';
|
|
9
9
|
import type { HostObservable, SlotEntryLike } from '../host-types';
|
|
10
10
|
import { bindForeign } from '../i18n';
|
|
11
11
|
import { diagError } from '../diag';
|
|
@@ -42,11 +42,33 @@ export function useSlotEntries(key: string): readonly unknown[] {
|
|
|
42
42
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* React 可作为元素类型使用的「组件包装」符号(memo / forwardRef / lazy)。
|
|
47
|
+
* 只认这三类,不做「任意 symbol `$$typeof`」放行——element / context / 伪造
|
|
48
|
+
* 对象都不是可渲染组件类型:放行后 React 会在为子元素创建 Fiber 时抛 #130,
|
|
49
|
+
* 而该错误发生在 EntryBoundary 自身的工作单元内(边界只捕获后代错误),会
|
|
50
|
+
* 冒泡到宿主的 SlotErrorBoundary 并废黜 conversation.composer.bar(接管整体
|
|
51
|
+
* 退回官方输入框)。收紧后这类坏条目回到静默跳过,与 v0.3.5 行为一致。
|
|
52
|
+
*/
|
|
53
|
+
const RENDERABLE_OBJECT_TYPES: readonly symbol[] = [
|
|
54
|
+
Symbol.for('react.memo'),
|
|
55
|
+
Symbol.for('react.forward_ref'),
|
|
56
|
+
Symbol.for('react.lazy'),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** 组件位是否为 React 可渲染类型(函数组件 / memo / forwardRef / lazy 包装)。 */
|
|
60
|
+
export function isRenderableComponent(value: unknown): boolean {
|
|
61
|
+
if (typeof value === 'function') return true;
|
|
62
|
+
if (value === null || typeof value !== 'object') return false;
|
|
63
|
+
const type = (value as { $$typeof?: unknown }).$$typeof;
|
|
64
|
+
return typeof type === 'symbol' && RENDERABLE_OBJECT_TYPES.includes(type);
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
/** single 槽位赢家 = entries 首个带组件的条目(register 即按优先级排序)。 */
|
|
46
68
|
export function firstComponentEntry(entries: readonly unknown[]): SlotEntryLike | null {
|
|
47
69
|
for (const candidate of entries) {
|
|
48
70
|
const record = candidate as SlotEntryLike | null | undefined;
|
|
49
|
-
if (record && typeof record === 'object' &&
|
|
71
|
+
if (record && typeof record === 'object' && isRenderableComponent(record.component)) return record;
|
|
50
72
|
}
|
|
51
73
|
return null;
|
|
52
74
|
}
|
|
@@ -61,7 +83,7 @@ export function listRows(entries: readonly unknown[]): SlotEntryLike[] {
|
|
|
61
83
|
const byId = new Map<unknown, SlotEntryLike>();
|
|
62
84
|
for (const candidate of entries) {
|
|
63
85
|
const record = candidate as (SlotEntryLike & { options?: { id?: unknown } }) | null | undefined;
|
|
64
|
-
if (!record || typeof record !== 'object' ||
|
|
86
|
+
if (!record || typeof record !== 'object' || !isRenderableComponent(record.component)) continue;
|
|
65
87
|
const id = (record as { options?: { id?: unknown } }).options?.id;
|
|
66
88
|
if (byId.has(id)) continue;
|
|
67
89
|
byId.set(id, record);
|
|
@@ -82,6 +104,76 @@ export function entryId(entry: SlotEntryLike, index: number): string {
|
|
|
82
104
|
return options?.id ?? `#${index}`;
|
|
83
105
|
}
|
|
84
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 座位「第三方遮蔽者」判据(单值座让位用)。
|
|
109
|
+
*
|
|
110
|
+
* 宿主的动态包守卫(dsh-cordis-client-runner 的 `guardedSlots`)对非 chain
|
|
111
|
+
* 槽位的 register 做两件事:① **强制**用页内遮蔽秩覆盖插件自报的 priority
|
|
112
|
+
* (`--nextPriority`,负数且唯一——插件写 priority:-1 也会被覆盖成分配值);
|
|
113
|
+
* ② fiber 名取 `dyn/<pluginId>`,而条目 `registrant` = `options.registrant ??
|
|
114
|
+
* fiber.name`。出厂自带占用者(如 client-ui-model-selection 的 ModelSelect)
|
|
115
|
+
* 不经过该守卫:不带 priority(≡0),registrant 也没有 `dyn/` 前缀。
|
|
116
|
+
*
|
|
117
|
+
* 据此把「第三方遮蔽者」与「出厂占用者」分开:前者应当让位(镜像它),
|
|
118
|
+
* 后者继续由 Codinput 自绘控件代表(镜像出厂条目会与自绘控件重复)。
|
|
119
|
+
* 负 priority 是主判据(分配秩即遮蔽意图);`dyn/` 前缀是兜底(防旧宿主/
|
|
120
|
+
* 未来守卫变化下 priority 未落进条目 options 的形态)。
|
|
121
|
+
*/
|
|
122
|
+
export function isShadowingOccupant(entry: SlotEntryLike): boolean {
|
|
123
|
+
const options = (entry as { options?: { priority?: number; registrant?: string } }).options;
|
|
124
|
+
const priority = options?.priority;
|
|
125
|
+
if (typeof priority === 'number' && priority < 0) return true;
|
|
126
|
+
const registrant = entry.registrant ?? options?.registrant;
|
|
127
|
+
return typeof registrant === 'string' && registrant.startsWith('dyn/');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 单条目渲染(座位镜像与单值座共用):官方 SessionEntry → renderEntry 的
|
|
132
|
+
* props 展开式 `{...standard(kit), sessionId, ...entryInjectProps, ...t,
|
|
133
|
+
* ...ownerProps}`——owner props 最后展开(如模型座的 `locked` 不被 kit 覆盖)。
|
|
134
|
+
* 逐条目 EntryBoundary 隔离:单个条目崩溃只丢本座位。
|
|
135
|
+
*/
|
|
136
|
+
export function renderSlotEntry(
|
|
137
|
+
entry: SlotEntryLike,
|
|
138
|
+
key: string,
|
|
139
|
+
options: {
|
|
140
|
+
sessionId: string;
|
|
141
|
+
standard: Record<string, unknown> | undefined;
|
|
142
|
+
/** 官方 renderSlot 第二参数的 owner props(如模型座 { locked })。 */
|
|
143
|
+
ownerProps?: Record<string, unknown> | undefined;
|
|
144
|
+
/** 条目崩溃时的兜底渲染(单值座让位用:回退自绘控件;缺省 null)。 */
|
|
145
|
+
fallback?: React.ReactNode;
|
|
146
|
+
},
|
|
147
|
+
): JSX.Element {
|
|
148
|
+
const { sessionId, standard, ownerProps, fallback } = options;
|
|
149
|
+
const Comp = entry.component as React.ComponentType<Record<string, unknown>>;
|
|
150
|
+
// 条目词典的 t(官方 localeSeat 同款);无 locale 或绑定失败 → 不下发,
|
|
151
|
+
// 组件按各自回退(如 enhancer 回退内置中文词典)。
|
|
152
|
+
let t: unknown;
|
|
153
|
+
try {
|
|
154
|
+
t = translatorFor(entry) ?? undefined;
|
|
155
|
+
} catch {
|
|
156
|
+
t = undefined;
|
|
157
|
+
}
|
|
158
|
+
let injected: Record<string, unknown> | null = null;
|
|
159
|
+
try {
|
|
160
|
+
injected = entryInjectProps(entry, sessionId);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
diagError('slot.seat.inject', error);
|
|
163
|
+
}
|
|
164
|
+
return createElement(
|
|
165
|
+
EntryBoundary,
|
|
166
|
+
{ key, fallback },
|
|
167
|
+
createElement(Comp, {
|
|
168
|
+
...(standard ?? {}),
|
|
169
|
+
sessionId,
|
|
170
|
+
...(injected ?? {}),
|
|
171
|
+
...(t !== undefined ? { t } : {}),
|
|
172
|
+
...(ownerProps ?? {}),
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
85
177
|
export type SnapshotSelectorHook = <S>(sel: (s: any) => S, eq?: (a: S, b: S) => boolean) => S;
|
|
86
178
|
|
|
87
179
|
/** 快照 store → use<Name> 选择器钩子(官方 observableHook 同款,按源缓存)。 */
|
|
@@ -113,38 +205,60 @@ export function standardHookPropName(name: string): string {
|
|
|
113
205
|
* 复刻):以 sessionId 调 inject,把 hooks 子表折成 use<Name> 钩子、其余
|
|
114
206
|
* 原样下传;keyedHooks 需要官方 keyed resolver 面,插件侧不可复现 → 跳过
|
|
115
207
|
* (现网条目无键控钩子)。结果按(条目 × 会话)缓存——官方同款缓存轴。
|
|
208
|
+
*
|
|
209
|
+
* 为什么额外记「失败」:官方 renderer 是在**宿主自己的 fiber 上下文里**调
|
|
210
|
+
* 这个 inject 闭包;插件侧从自己的 fiber 调会丢掉条目所属插件的 ctx 绑定,
|
|
211
|
+
* 于是依赖宿主服务面的 inject 会抛 `cannot get property "remote.session"
|
|
212
|
+
* without inject`(实测 dsh-model-accordion 0.2.1 的 `directoryFor`)。这是
|
|
213
|
+
* 插件侧不可复现的边界——单值座镜像据此让位降级(回退自绘控件),而不是把
|
|
214
|
+
* 空 props 下发给一个必然崩溃的组件。
|
|
116
215
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
216
|
+
interface InjectBound {
|
|
217
|
+
props: Record<string, unknown> | null;
|
|
218
|
+
failed: boolean;
|
|
219
|
+
}
|
|
220
|
+
const entryInjectCache = new WeakMap<object, Map<string, InjectBound>>();
|
|
221
|
+
|
|
222
|
+
function boundInject(entry: SlotEntryLike, sessionId: string): InjectBound {
|
|
120
223
|
let perSession = entryInjectCache.get(entry);
|
|
121
224
|
if (!perSession) {
|
|
122
225
|
perSession = new Map();
|
|
123
226
|
entryInjectCache.set(entry, perSession);
|
|
124
227
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
bound = rest;
|
|
228
|
+
const cached = perSession.get(sessionId);
|
|
229
|
+
if (cached) return cached;
|
|
230
|
+
let bound: InjectBound = { props: null, failed: false };
|
|
231
|
+
try {
|
|
232
|
+
const face = entry.inject?.(sessionId);
|
|
233
|
+
if (face && typeof face === 'object') {
|
|
234
|
+
const { hooks: _hooks, keyedHooks: _keyed, ...rest } = face as Record<string, unknown> & {
|
|
235
|
+
hooks?: Record<string, unknown>;
|
|
236
|
+
keyedHooks?: Record<string, unknown>;
|
|
237
|
+
};
|
|
238
|
+
void _keyed;
|
|
239
|
+
for (const [name, source] of Object.entries(_hooks ?? {})) {
|
|
240
|
+
const hook = snapshotHookFor(source);
|
|
241
|
+
if (hook) rest[standardHookPropName(name)] = hook;
|
|
140
242
|
}
|
|
141
|
-
|
|
142
|
-
diagError('slot.inject', error);
|
|
143
|
-
bound = null;
|
|
243
|
+
bound = { props: rest, failed: false };
|
|
144
244
|
}
|
|
145
|
-
|
|
245
|
+
} catch (error) {
|
|
246
|
+
diagError('slot.inject', error);
|
|
247
|
+
bound = { props: null, failed: true };
|
|
146
248
|
}
|
|
147
|
-
|
|
249
|
+
perSession.set(sessionId, bound);
|
|
250
|
+
return bound;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function entryInjectProps(entry: SlotEntryLike, sessionId: string): Record<string, unknown> | null {
|
|
254
|
+
if (typeof entry.inject !== 'function') return null;
|
|
255
|
+
return boundInject(entry, sessionId).props;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 该条目 inject 是否抛错(宿主 ctx 绑定面缺失的判据,见上)。 */
|
|
259
|
+
export function entryInjectFailed(entry: SlotEntryLike, sessionId: string): boolean {
|
|
260
|
+
if (typeof entry.inject !== 'function') return false;
|
|
261
|
+
return boundInject(entry, sessionId).failed;
|
|
148
262
|
}
|
|
149
263
|
|
|
150
264
|
/** 官方 renderer 的 locale 座位:locale.bind(entry.locale),按 ns 缓存。 */
|
package/src/client/host-types.ts
CHANGED
|
@@ -61,7 +61,15 @@ export interface SlotEntryLike {
|
|
|
61
61
|
readonly inject?: (...args: unknown[]) => Record<string, unknown>;
|
|
62
62
|
/** 条目词典命名空间(locale.bind(ns) 即官方传给组件的 t)。 */
|
|
63
63
|
readonly locale?: string;
|
|
64
|
+
/** 归属(options.registrant ?? fiber.name;动态包为 dyn/<pluginId>)。 */
|
|
64
65
|
readonly registrant?: string;
|
|
66
|
+
/** 注册选项切面:priority 负数 = 动态包被守卫分配的页内遮蔽秩(见 slotEntry)。 */
|
|
67
|
+
readonly options?: {
|
|
68
|
+
readonly id?: string;
|
|
69
|
+
readonly order?: number;
|
|
70
|
+
readonly priority?: number;
|
|
71
|
+
readonly registrant?: string;
|
|
72
|
+
};
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
// ---------- input machine ----------
|