@dsh-plus/subagent-model 0.1.10 → 0.2.0

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/src/catalog.ts DELETED
@@ -1,127 +0,0 @@
1
- /**
2
- * 提供商/模型/思考档位目录:配置卡片的下拉数据源。
3
- * 镜像官方 api-proxy `buildModelCatalog`(session.models RPC)的构建方式:
4
- * `ctx.llm.listProviders()` → `listModels()` → `resolveModelInfo()`,
5
- * 附 `ctx.subagents.list()` 的已注册子代理 provider 名(卡片按此行合成空行)。
6
- * 目录仅作建议性数据源,不参与运行期路由。
7
- * @module subagent-model/catalog
8
- */
9
- import type { Context } from '@deepseek-ai/cordis'
10
- import type { LlmReasoningEffortInfo } from '@deepseek-ai/dsh-llm'
11
-
12
- export interface CatalogEffort {
13
- id: string
14
- name: string
15
- description?: string
16
- }
17
-
18
- export interface CatalogModel {
19
- id: string
20
- name: string
21
- description?: string
22
- reasoning?: {
23
- defaultEffort?: string
24
- efforts: CatalogEffort[]
25
- }
26
- }
27
-
28
- export interface CatalogProvider {
29
- id: string
30
- name: string
31
- models: CatalogModel[]
32
- }
33
-
34
- export interface CatalogFailure {
35
- id: string
36
- name: string
37
- message: string
38
- }
39
-
40
- export interface ModelCatalog {
41
- providers: CatalogProvider[]
42
- failures: CatalogFailure[]
43
- /** 已注册的子代理 provider 名(spawn/fork/…),卡片据此合成配置行。 */
44
- subagentProviders: string[]
45
- }
46
-
47
- function toEffort(effort: LlmReasoningEffortInfo): CatalogEffort {
48
- return {
49
- id: String(effort.id),
50
- name: effort.name,
51
- ...(effort.description === undefined ? {} : { description: effort.description }),
52
- }
53
- }
54
-
55
- /**
56
- * 构建目录。单个提供商失败只进入 failures,不拖垮其余组;
57
- * 无模型的组被丢弃(与官方 buildModelCatalog 同语义)。
58
- */
59
- export async function buildModelCatalog(ctx: Context): Promise<ModelCatalog> {
60
- const llm = ctx.get('llm')
61
- if (llm === undefined) {
62
- return {
63
- providers: [],
64
- failures: [],
65
- subagentProviders: subagentProvidersOf(ctx),
66
- }
67
- }
68
- const catalog = await Promise.all(
69
- llm.listProviders().map(async (provider) => {
70
- try {
71
- const models = await llm.listModels(provider.id)
72
- const entries = await Promise.all(
73
- models.map(async (model) => {
74
- const resolved = await llm.resolveModelInfo(provider.id, model.id)
75
- const reasoning =
76
- resolved.reasoning === undefined
77
- ? undefined
78
- : {
79
- efforts: resolved.reasoning.efforts.map(toEffort),
80
- ...(resolved.reasoning.defaultEffort === undefined
81
- ? {}
82
- : {
83
- defaultEffort: String(resolved.reasoning.defaultEffort),
84
- }),
85
- }
86
- return {
87
- id: model.id,
88
- name: model.name,
89
- ...(model.description === undefined ? {} : { description: model.description }),
90
- ...(reasoning === undefined ? {} : { reasoning }),
91
- }
92
- }),
93
- )
94
- return {
95
- kind: 'group' as const,
96
- group: { id: provider.id, name: provider.name, models: entries },
97
- }
98
- } catch (error) {
99
- return {
100
- kind: 'failure' as const,
101
- failure: {
102
- id: provider.id,
103
- name: provider.name,
104
- message: error instanceof Error ? error.message : String(error),
105
- },
106
- }
107
- }
108
- }),
109
- )
110
- return {
111
- providers: catalog
112
- .filter((item) => item.kind === 'group' && item.group.models.length > 0)
113
- .map((item) => (item.kind === 'group' ? item.group : undefined))
114
- .filter((group): group is CatalogProvider => group !== undefined),
115
- failures: catalog
116
- .filter((item) => item.kind === 'failure')
117
- .map((item) => (item.kind === 'failure' ? item.failure : undefined))
118
- .filter((failure): failure is CatalogFailure => failure !== undefined),
119
- subagentProviders: subagentProvidersOf(ctx),
120
- }
121
- }
122
-
123
- /** 已注册的子代理 provider 名(llm 服务缺失时同样返回,卡片仍可渲染)。 */
124
- function subagentProvidersOf(ctx: Context): string[] {
125
- const subagents = ctx.get('subagents')
126
- return subagents === undefined ? [] : [...subagents.list()]
127
- }
package/src/client/api.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * 自定义端点通道:仅剩「模型目录」(配置读写已迁移到官方 settingsScope
3
- * 传输,见 scope.ts 与 card.tsx)。
4
- * @module subagent-model/client/api
5
- */
6
-
7
- export interface CatalogEffort {
8
- id: string
9
- name: string
10
- description?: string
11
- }
12
-
13
- export interface CatalogModel {
14
- id: string
15
- name: string
16
- description?: string
17
- reasoning?: {
18
- defaultEffort?: string
19
- efforts: CatalogEffort[]
20
- }
21
- }
22
-
23
- export interface CatalogProvider {
24
- id: string
25
- name: string
26
- models: CatalogModel[]
27
- }
28
-
29
- export interface ModelCatalog {
30
- providers: CatalogProvider[]
31
- failures: { id: string; name: string; message: string }[]
32
- subagentProviders: string[]
33
- }
34
-
35
- const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
36
-
37
- async function parse<T>(res: Response): Promise<T> {
38
- const body = (await res.json()) as T & { error?: string }
39
- if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`)
40
- return body
41
- }
42
-
43
- export async function fetchCatalog(): Promise<ModelCatalog> {
44
- return parse<ModelCatalog>(await fetch(ROUTE_CATALOG, { credentials: 'same-origin' }))
45
- }
@@ -1,340 +0,0 @@
1
- /**
2
- * 「子代理模型配置」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
3
- * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/CheckRow/SelectField)。
4
- * 配置读写走官方 settingsScope 传输:value 为 schema 解析后的命名空间值
5
- * (enabled + entries),行集合 = 目录返回的已注册子代理 provider
6
- * ∪ 已配置条目,保存时全量写回 entries(未配置的新 provider 行以默认空值
7
- * 落盘,自文档化);「模型目录」为唯一保留的自定义端点。
8
- * @module subagent-model/client/card
9
- */
10
-
11
- import {
12
- CardChrome,
13
- type CardStatusState,
14
- CheckRow,
15
- IDLE_STATUS,
16
- type Scope,
17
- SelectField,
18
- type SelectOption,
19
- type SettingsApi,
20
- } from '@dsh-plus/shared/client'
21
- import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
22
- import { SETTINGS_NS } from '../ns.ts'
23
- import { type CatalogProvider, fetchCatalog, type ModelCatalog } from './api.ts'
24
- import {
25
- type ConfigValue,
26
- type Draft,
27
- type DraftRow,
28
- draftFrom,
29
- EMPTY_ROW,
30
- toPatch,
31
- } from './draft.ts'
32
-
33
- export interface CardProps {
34
- t(key: string): string
35
- scope: Scope
36
- api: SettingsApi
37
- }
38
-
39
- const EFFORT_INHERIT = 'inherit'
40
- const EFFORT_DEFAULT = 'default'
41
-
42
- function unknownOption(value: string, label: string): SelectOption {
43
- return { value, label: `${value}(${label})` }
44
- }
45
-
46
- function providerOptions(
47
- catalog: ModelCatalog | null,
48
- row: DraftRow,
49
- t: (k: string) => string,
50
- ): SelectOption[] {
51
- const options: SelectOption[] = [{ value: '', label: t('inheritProvider') }]
52
- for (const provider of catalog?.providers ?? [])
53
- options.push({ value: provider.id, label: provider.name })
54
- if (row.provider.length > 0 && !options.some((o) => o.value === row.provider)) {
55
- options.push(unknownOption(row.provider, t('unknownValue')))
56
- }
57
- return options
58
- }
59
-
60
- function modelOptions(
61
- catalog: ModelCatalog | null,
62
- row: DraftRow,
63
- t: (k: string) => string,
64
- ): SelectOption[] {
65
- const options: SelectOption[] = [{ value: '', label: t('inheritModel') }]
66
- const provider = catalog?.providers.find((p) => p.id === row.provider)
67
- for (const model of provider?.models ?? []) options.push({ value: model.id, label: model.name })
68
- if (row.model.length > 0 && !options.some((o) => o.value === row.model)) {
69
- options.push(unknownOption(row.model, t('unknownValue')))
70
- }
71
- return options
72
- }
73
-
74
- function effortOptions(
75
- catalog: ModelCatalog | null,
76
- row: DraftRow,
77
- t: (k: string) => string,
78
- ): SelectOption[] {
79
- const options: SelectOption[] = [
80
- { value: EFFORT_INHERIT, label: t('effortInherit') },
81
- { value: EFFORT_DEFAULT, label: t('effortDefault') },
82
- ]
83
- const provider = catalog?.providers.find((p) => p.id === row.provider)
84
- const model = provider?.models.find((m) => m.id === row.model)
85
- for (const effort of model?.reasoning?.efforts ?? [])
86
- options.push({ value: effort.id, label: effort.name })
87
- if (row.reasoningEffort.length > 0 && !options.some((o) => o.value === row.reasoningEffort)) {
88
- options.push(unknownOption(row.reasoningEffort, t('unknownValue')))
89
- }
90
- return options
91
- }
92
-
93
- interface RowBlockProps {
94
- name: string
95
- row: DraftRow
96
- catalog: ModelCatalog | null
97
- disabled: boolean
98
- t(key: string): string
99
- onEdit(row: DraftRow): void
100
- }
101
-
102
- function RowBlock(props: RowBlockProps): ReactElement {
103
- const { name, row, catalog, disabled, t, onEdit } = props
104
- const invalidModel = row.model.length > 0 && row.provider.length === 0
105
- return (
106
- <div className="dsm-row">
107
- <div className="dsm-rowHead">
108
- <span className="dsm-rowName">{name}</span>
109
- <CheckRow
110
- prefix="dsm"
111
- id={`dsm-row-${name}`}
112
- label={t('rowEnabled')}
113
- checked={row.enabled}
114
- disabled={disabled}
115
- onEdit={(v) => onEdit({ ...row, enabled: v })}
116
- />
117
- </div>
118
- <p className="dsm-rowHint">{t('rowHint')}</p>
119
- <SelectField
120
- prefix="dsm"
121
- id={`dsm-provider-${name}`}
122
- label={t('provider')}
123
- hint={t('providerHint')}
124
- value={row.provider}
125
- options={providerOptions(catalog, row, t)}
126
- disabled={disabled}
127
- onEdit={(v) => onEdit({ ...row, provider: v })}
128
- />
129
- <SelectField
130
- prefix="dsm"
131
- id={`dsm-model-${name}`}
132
- label={t('model')}
133
- hint={t('modelHint')}
134
- value={row.model}
135
- options={modelOptions(catalog, row, t)}
136
- disabled={disabled || row.provider.length === 0}
137
- invalid={invalidModel}
138
- invalidLabel={t('invalidModel')}
139
- onEdit={(v) => onEdit({ ...row, model: v })}
140
- />
141
- <SelectField
142
- prefix="dsm"
143
- id={`dsm-effort-${name}`}
144
- label={t('effort')}
145
- hint={t('effortHint')}
146
- value={row.reasoningEffort}
147
- options={effortOptions(catalog, row, t)}
148
- disabled={disabled}
149
- onEdit={(v) => onEdit({ ...row, reasoningEffort: v })}
150
- />
151
- </div>
152
- )
153
- }
154
-
155
- export function SubagentModelCard(props: CardProps): ReactElement | null {
156
- const { t, scope, api } = props
157
- const snapshot = useSyncExternalStore(
158
- (listener: () => void) => scope.subscribe(listener),
159
- () => scope.getSnapshot(),
160
- )
161
- const value = snapshot.value as ConfigValue | undefined
162
- const [open, setOpen] = useState(false)
163
- const [draft, setDraft] = useState<Draft | null>(null)
164
- const [catalog, setCatalog] = useState<ModelCatalog | null>(null)
165
- const [catalogFailed, setCatalogFailed] = useState(false)
166
- const [saving, setSaving] = useState(false)
167
- const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
168
-
169
- // 播种草稿(行集 = 已配置条目);catalog 到达后只补缺失的 provider 空行,
170
- // 不覆盖在途编辑;后续 Host 更新同样不覆盖。
171
- useEffect(() => {
172
- if (value === undefined) return
173
- setDraft((current) => {
174
- if (current === null) return draftFrom(value, catalog)
175
- if (catalog === null) return current
176
- const rows = { ...current.rows }
177
- let changed = false
178
- for (const name of catalog.subagentProviders) {
179
- if (name in rows) continue
180
- rows[name] = { ...EMPTY_ROW }
181
- changed = true
182
- }
183
- return changed ? { ...current, rows } : current
184
- })
185
- }, [value, catalog])
186
-
187
- useEffect(() => {
188
- if (!open || catalog !== null) return
189
- let alive = true
190
- fetchCatalog()
191
- .then((loaded) => {
192
- if (!alive) return
193
- setCatalog(loaded)
194
- setCatalogFailed(false)
195
- })
196
- .catch(() => {
197
- if (alive) setCatalogFailed(true)
198
- })
199
- return () => {
200
- alive = false
201
- }
202
- }, [open, catalog])
203
-
204
- const dirty = useMemo(
205
- () =>
206
- value !== undefined &&
207
- draft !== null &&
208
- JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFrom(value, catalog))),
209
- [value, draft, catalog],
210
- )
211
- const invalid = useMemo(
212
- () =>
213
- draft !== null &&
214
- Object.values(draft.rows).some((row) => row.model.length > 0 && row.provider.length === 0),
215
- [draft],
216
- )
217
-
218
- if (value === undefined || draft === null) {
219
- return (
220
- <li className="dsm-card">
221
- <p className="dsm-empty">{t('loading')}</p>
222
- </li>
223
- )
224
- }
225
- const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
226
- setDraft({ ...draft, [key]: editValue })
227
- setStatus(IDLE_STATUS)
228
- }
229
- const editRow = (name: string, row: DraftRow): void => {
230
- setDraft({ ...draft, rows: { ...draft.rows, [name]: row } })
231
- setStatus(IDLE_STATUS)
232
- }
233
- const onSave = (): void => {
234
- setSaving(true)
235
- const revision = scope.getSnapshot().revision
236
- api
237
- .update({
238
- ns: SETTINGS_NS,
239
- patch: toPatch(draft),
240
- ...(revision !== undefined ? { expectedRevision: revision } : {}),
241
- })
242
- .then(async (response) => {
243
- if (!response.result.ok) {
244
- throw new Error(response.result.error?.message ?? t('saveFailed'))
245
- }
246
- await scope.load()
247
- const next = scope.getSnapshot().value as ConfigValue | undefined
248
- if (next !== undefined) setDraft(draftFrom(next, catalog))
249
- setStatus(IDLE_STATUS)
250
- })
251
- .catch((error: unknown) => {
252
- setStatus({
253
- kind: 'error',
254
- text: `${t('saveFailed')}${error instanceof Error ? error.message : ''}`,
255
- })
256
- })
257
- .finally(() => setSaving(false))
258
- }
259
- const onRetryCatalog = (): void => {
260
- setCatalog(null)
261
- setCatalogFailed(false)
262
- }
263
-
264
- const disabled = !snapshot.writable
265
- const rowNames = Object.keys(draft.rows).sort()
266
- const providerCount = (catalog?.providers ?? []).length
267
- return (
268
- <CardChrome
269
- prefix="dsm"
270
- title={t('title')}
271
- description={t('description')}
272
- open={open}
273
- onToggle={setOpen}
274
- statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
275
- dirty={dirty}
276
- dirtyLabel={t('unsaved')}
277
- readOnlyNotice={disabled ? t('readOnly') : undefined}
278
- status={status}
279
- actions={[
280
- {
281
- key: 'discard',
282
- label: t('discard'),
283
- disabled: !dirty || saving,
284
- onClick: () => {
285
- setDraft(draftFrom(value, catalog))
286
- setStatus(IDLE_STATUS)
287
- },
288
- },
289
- {
290
- key: 'save',
291
- label: t(saving ? 'saving' : 'save'),
292
- variant: 'primary',
293
- disabled: !dirty || invalid || saving || disabled,
294
- onClick: onSave,
295
- },
296
- ]}
297
- >
298
- <CheckRow
299
- prefix="dsm"
300
- id="dsm-enabled"
301
- label={t('enabled')}
302
- checked={draft.enabled}
303
- disabled={disabled}
304
- onEdit={(v) => edit('enabled', v)}
305
- />
306
- {catalogFailed ? (
307
- <div className="dsm-banner" role="status">
308
- <span>{t('catalogError')}</span>
309
- <button type="button" className="dsm-bannerRetry" onClick={onRetryCatalog}>
310
- {t('catalogRetry')}
311
- </button>
312
- </div>
313
- ) : null}
314
- {catalog === null && !catalogFailed ? (
315
- <p className="dsm-empty" role="status">
316
- {t('catalogLoading')}
317
- </p>
318
- ) : null}
319
- {rowNames.length === 0 ? (
320
- <p className="dsm-empty" role="status">
321
- {t('noRows')}
322
- </p>
323
- ) : null}
324
- {rowNames.map((name) => (
325
- <RowBlock
326
- key={name}
327
- name={name}
328
- row={draft.rows[name] ?? { ...EMPTY_ROW }}
329
- catalog={catalog}
330
- disabled={disabled}
331
- t={t}
332
- onEdit={(row) => editRow(name, row)}
333
- />
334
- ))}
335
- <p className="dsm-hint dsm-rowHint">{providerCount > 0 ? t('rowDesc') : ''}</p>
336
- </CardChrome>
337
- )
338
- }
339
-
340
- export type { CatalogProvider }
@@ -1,78 +0,0 @@
1
- /**
2
- * 浏览器半入口:注册 locale 字典 + 向 settings.plugin.item 插槽注册「子代理模型配置」卡片。
3
- * 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
4
- * (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
5
- *
6
- * 类型说明:浏览器半只用到 slots/locale/connection/remote 的很窄一面,
7
- * scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现)。
8
- * rc7 起该插槽为 keyed 槽位:key 必须是卡片编辑的 settings 命名空间
9
- * (即服务端 settingsNamespace() 注册的同一字面量,见 ../ns.ts),
10
- * 官方配置页按此 key 与 Host 已注册命名空间配对分发。
11
- * 配置读写经官方 settings RPC 直连(rc7 起第三方命名空间全量开放;
12
- * 不复用 settingsScope 服务——非 loopback 页面下它无数据);
13
- * 自定义端点仅剩「模型目录」(api.ts)。
14
- * @module @dsh-plus/subagent-model/client
15
- */
16
- import type { Context } from '@deepseek-ai/cordis'
17
- import { createApiScope } from '@dsh-plus/shared/client'
18
- import { SETTINGS_NS } from '../ns.ts'
19
- import { SubagentModelCard } from './card.tsx'
20
- import { en, NS, zh } from './i18n.ts'
21
- import { injectStyle } from './styles.ts'
22
-
23
- export const name = 'dsh-plus-subagent-model'
24
-
25
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
26
- export const inject = ['slots', 'locale', 'connection', 'remote'] as const
27
-
28
- interface SlotsLike {
29
- inject(key: string, callback: () => unknown): unknown
30
- register(options: Record<string, unknown>, component: unknown): () => void
31
- }
32
-
33
- interface LocaleLike {
34
- register(ns: string, dict: { zh: Record<string, string>; en: Record<string, string> }): () => void
35
- bind(ns: string): (key: string) => string
36
- }
37
-
38
- interface RemoteLike {
39
- $on(event: string, listener: (payload?: unknown) => void): () => void
40
- }
41
-
42
- interface ConnectionLike {
43
- api: { settings: Parameters<typeof createApiScope>[0] }
44
- }
45
-
46
- interface ClientContext {
47
- slots: SlotsLike
48
- locale: LocaleLike
49
- get(key: 'connection'): ConnectionLike
50
- get(key: 'remote'): RemoteLike
51
- on(event: string, listener: () => void): () => void
52
- effect(execute: () => () => void, label?: string): unknown
53
- }
54
-
55
- export function apply(ctx: Context): void {
56
- const c = ctx as unknown as ClientContext
57
- const tag = injectStyle()
58
- c.effect(
59
- () => () => {
60
- tag?.remove()
61
- },
62
- 'subagent-model: style',
63
- )
64
- c.effect(() => c.locale.register(NS, { zh, en }), 'subagent-model: locale')
65
- const api = c.get('connection').api.settings
66
- const scope = createApiScope(api, SETTINGS_NS, c, 'subagent-model: settings scope')
67
- c.slots.inject('settings.plugin.item', () =>
68
- c.slots.register(
69
- {
70
- name: 'settings.plugin.item',
71
- key: SETTINGS_NS,
72
- locale: NS,
73
- inject: () => ({ t: c.locale.bind(NS), scope, api }),
74
- },
75
- SubagentModelCard,
76
- ),
77
- )
78
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * 配置卡片的可编辑草稿模型:ConfigValue ↔ Draft 转换与提交补丁(纯函数,
3
- * 与渲染分离以便单测)。行集合 = 目录返回的已注册子代理 provider ∪ 已配置条目;
4
- * 未配置的新 provider 行以默认空值落盘(自文档化)。
5
- * @module subagent-model/client/draft
6
- */
7
- import type { ModelCatalog } from './api.ts'
8
-
9
- /** settings 命名空间的解析值(无 secret 字段)。 */
10
- export interface ConfigValue {
11
- enabled: boolean
12
- entries: Record<string, WireEntry>
13
- }
14
-
15
- export interface WireEntry {
16
- enabled: boolean
17
- provider: string
18
- model: string
19
- reasoningEffort: string
20
- }
21
-
22
- export interface DraftRow {
23
- enabled: boolean
24
- provider: string
25
- model: string
26
- reasoningEffort: string
27
- }
28
-
29
- export interface Draft {
30
- enabled: boolean
31
- rows: Record<string, DraftRow>
32
- }
33
-
34
- export const EMPTY_ROW: DraftRow = {
35
- enabled: false,
36
- provider: '',
37
- model: '',
38
- reasoningEffort: 'inherit',
39
- }
40
-
41
- /** 行集 = 目录 subagentProviders ∪ 已配置条目;目录行缺省为空行。 */
42
- export function draftFrom(value: ConfigValue, catalog: ModelCatalog | null): Draft {
43
- const rows: Record<string, DraftRow> = {}
44
- const names = new Set<string>()
45
- for (const name of catalog?.subagentProviders ?? []) names.add(name)
46
- for (const name of Object.keys(value.entries).sort()) names.add(name)
47
- for (const name of names) {
48
- const entry = value.entries[name]
49
- rows[name] =
50
- entry === undefined
51
- ? { ...EMPTY_ROW }
52
- : {
53
- enabled: entry.enabled,
54
- provider: entry.provider,
55
- model: entry.model,
56
- reasoningEffort: entry.reasoningEffort,
57
- }
58
- }
59
- return { enabled: value.enabled, rows }
60
- }
61
-
62
- export function toPatch(draft: Draft): Record<string, unknown> {
63
- // entries 按键排序物化:dirty 比较走 JSON.stringify,而草稿的播种路径
64
- // (先 entries 后补目录空行)与 draftFrom(先目录后 entries)键序不同,
65
- // 不排序会出现「未改动却永久 dirty」的假阳性。
66
- const entries: Record<string, DraftRow> = {}
67
- for (const name of Object.keys(draft.rows).sort()) entries[name] = draft.rows[name]
68
- return { enabled: draft.enabled, entries }
69
- }