@lienze/dsh-subagent-model-policy 0.0.0-stage → 0.3.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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/README_en.md +82 -0
- package/client.js +1271 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +1 -0
- package/index.js +810 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/management.js +673 -0
- package/package.json +74 -4
- package/policy.js +119 -0
package/client.js
ADDED
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
/* Browser-only lazy module. No Harness Client package is a runtime dependency. */
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: '@lienze/dsh-subagent-model-policy',
|
|
4
|
+
factory(require) {
|
|
5
|
+
'use strict';
|
|
6
|
+
const React = require('react');
|
|
7
|
+
const { createElement: h, useCallback, useEffect, useId, useRef, useState } = React;
|
|
8
|
+
const ID = '@lienze/dsh-subagent-model-policy';
|
|
9
|
+
const KIND = 'lez-subagent-policy';
|
|
10
|
+
const NS = 'lez.subagentPolicy';
|
|
11
|
+
const en = {
|
|
12
|
+
title: 'Subagent Model Policy', open: 'Open subagent model policy', description: 'Strategies, model pool and delegation recursion limits in this session.',
|
|
13
|
+
scope: 'Current session', loading: 'Reading…', saving: 'Saving…', refreshing: 'Refreshing…', busy: 'Working…',
|
|
14
|
+
refresh: 'Refresh server state', cancel: 'Cancel wait', retry: 'Try again', error: 'Request failed', serverError: 'The Host returned no result.',
|
|
15
|
+
noModelsSelected: 'No models selected', badResponse: 'The Host returned an invalid policy snapshot.', missingCommand: 'The Host did not recognize /subagent-policy.', staleResponse: 'A response for a different session was rejected.',
|
|
16
|
+
conflict: 'Version conflict', conflictHelp: 'The server has a newer revision. Your draft is preserved. Load the server version to continue.', loadServer: 'Load server version', mustRefresh: 'Refresh is required before another write.',
|
|
17
|
+
readOnly: 'This session cannot edit the policy. The inherited snapshot is read-only.', noSession: 'Select a session to view its policy.', noSnapshot: 'No policy snapshot loaded.',
|
|
18
|
+
navLabel: 'Policy management pages', pageStrategies: 'Choose strategy', pageModels: 'Model pool', pageRecords: 'Execution insights',
|
|
19
|
+
sessionTitle: 'Strategy switch', policyLabelOn: 'Strategy switch: enabled', policyLabelOff: 'Strategy switch: not enabled',
|
|
20
|
+
closedSummary: 'Not enabled; the Host default applies (usually follows the parent model).',
|
|
21
|
+
activeLine: 'Enabled “{name}” · {type} · {target}', targetFixed: '{name}', targetAuto: '{count} candidates',
|
|
22
|
+
strategies: 'Strategies', countSuffix: '{count} saved', newStrategy: 'New strategy', emptyStrategies: 'No strategies yet. Create one.',
|
|
23
|
+
activeBadge: 'Currently enabled', defaultBadge: 'New-session default', defaultStaleBadge: 'New-session default (source not updated)', adjustBadge: 'Needs adjustment', stateOff: 'Not enabled',
|
|
24
|
+
legacyBadge: 'Legacy follow (read-only)', legacyHint: 'This legacy strategy followed the parent model. Saving it as fixed or Agent-chosen converts it explicitly.',
|
|
25
|
+
edit: 'Edit', editLabel: 'Edit strategy: {name}', enable: 'Enable', enableLabel: 'Enable strategy: {name}', disable: 'Disable', disableLabel: 'Disable strategy: {name}',
|
|
26
|
+
setDefault: 'Set as new-session default', setDefaultLabel: 'Set as new-session default: {name}', delete: 'Delete', deleteLabel: 'Delete strategy: {name}',
|
|
27
|
+
deleteActiveHint: 'Currently enabled; close it before deleting.', deleteDefaultHint: 'Source of the new-session default; change or clear the default before deleting.', needsAdjustHint: 'Referenced models are not in the session pool; adjust it before enabling.',
|
|
28
|
+
newStrategyTitle: 'New strategy', editStrategyTitle: 'Edit strategy', back: '← Back to strategies', backModels: '← Back to models', unsavedBadge: 'Not saved', savedBadge: 'Saved',
|
|
29
|
+
detailSubNew: 'Saving still does not enable it; after enabling, it takes effect on the next main-Agent model call.',
|
|
30
|
+
detailSubSaved: 'Saving only updates the strategy library; after enabling, it takes effect on the next main-Agent model call.',
|
|
31
|
+
detailSubActive: 'Currently enabled in this session; saving takes effect on the next main-Agent model call.',
|
|
32
|
+
convertedHint: 'This legacy strategy followed the parent model. Saving as fixed or Agent-chosen is an explicit conversion.',
|
|
33
|
+
nameLabel: 'Strategy name', namePlaceholder: 'e.g. Implementation default', typeLabel: 'Strategy type', typeFixed: 'Fixed model', typeAuto: 'Agent chooses',
|
|
34
|
+
typeHintFixed: 'Delegations use only the selected model; no automatic replacement.', typeHintAuto: 'Pick candidates from the pool and describe the choice; no scoring or silent fallback.',
|
|
35
|
+
fixedPanel: 'Fixed model', fixedHint: 'Choose one model from the saved session pool.', autoPanel: 'Candidate subset', autoHint: 'Pick candidates from the session pool; the agent decides only inside this subset.',
|
|
36
|
+
noPoolFixed: 'The session pool is empty. Add and save models on the model pool page first.', noPoolAuto: 'The session pool is empty. Add and save models on the model pool page first.',
|
|
37
|
+
textLabel: 'Selection note', textHint: 'The note only informs selection; it grants no new authorization.', textCount: '{count}/{max}',
|
|
38
|
+
saveStrategy: 'Save strategy', discard: 'Discard changes', cancelEdit: 'Cancel',
|
|
39
|
+
formError: 'Correct the marked fields before saving.', errNameEmpty: 'Enter a strategy name.', errNameLong: 'The strategy name is too long.', errNameDup: 'Another strategy already uses this name.',
|
|
40
|
+
errFixed: 'Select one fixed model.', errModels: 'Select at least one candidate.', errText: 'The selection note is too long.',
|
|
41
|
+
staleRef: 'Outside the pool', staleRefHint: 'The reference is kept; adjust it before enabling.',
|
|
42
|
+
modelsTitle: 'Model pool', modelsSub: 'Select the models this session allows as strategy candidates.', modelCount: '{count} candidates',
|
|
43
|
+
selectAll: 'Select all', clearAll: 'Clear all', selectedCount: 'Selected {count}', savedCount: 'Saved: {count} models',
|
|
44
|
+
poolDirty: 'Unsaved model-pool changes; they take effect only after saving.', poolImpact: 'The pool matches the saved state.',
|
|
45
|
+
poolImpactRemove: 'On save, remove: {items}.', poolImpactProtect: 'The currently enabled strategy references some removals; saving will be blocked.',
|
|
46
|
+
poolImpactAdd: 'On save, add: {items}; no strategy is changed automatically.', poolError: 'The model pool was not saved.',
|
|
47
|
+
poolBlockedActive: 'Cannot save: the currently enabled strategy “{name}” still references {models}. Adjust that reference, or close the strategy before removing them.',
|
|
48
|
+
savePool: 'Save model pool', discardPool: 'Discard pool changes', emptyModels: 'No candidates are available in this session.',
|
|
49
|
+
poolSync: 'Refresh candidates', poolSyncConfirm: 'Remove {count} candidate reference(s) the Host no longer authorizes or cannot resolve, from the pool and from strategies?',
|
|
50
|
+
poolSynced: 'Candidates refreshed: removed {count} stale reference(s) ({names}).', poolSyncedNone: 'The candidate pool is already up to date.',
|
|
51
|
+
poolToggleLabel: 'Model pool: {name}', abilityCard: 'Ability card', abilityCardLabel: 'Open ability card: {name} ({provider})', abilityTitle: 'Ability card',
|
|
52
|
+
purposeLabel: 'Short description', noteLabel: 'General usage note', noPurpose: 'No purpose recorded',
|
|
53
|
+
noteHintUseCase: 'Describe use cases and limits only; do not record prices or performance conclusions.',
|
|
54
|
+
noteHintEffect: 'Saving the note only: it takes effect on the next main-Agent model call.',
|
|
55
|
+
saveNote: 'Save note', discardNote: 'Discard note changes', noteSaved: 'Ability card saved; it takes effect on the next main-Agent model call.',
|
|
56
|
+
reasoningLine: 'Reasoning levels: {names}', reasoningDefault: ' (default {name})', reasoningNone: 'Reasoning levels: —',
|
|
57
|
+
unavailableHint: 'Currently not authorized or available; it cannot be added to the pool.',
|
|
58
|
+
limitsTitle: 'Subagent concurrency and recursion', sharedBadge: 'Shared by session',
|
|
59
|
+
limitsSub: 'Shared by the whole session; an actual delegation must satisfy both the Host and session limits.',
|
|
60
|
+
hostCeiling: 'Host ceilings: maximum recursion depth {depth}; concurrent subagents {active} (read from the Host).',
|
|
61
|
+
hostCeilingUnknown: 'Host depth ceiling: {depth}. The Host concurrent-subagent ceiling cannot be read or verified right now.{reason} This plugin does not guess it; the Host still enforces its own capacity.',
|
|
62
|
+
hostCeilingReason: ' Host reason: {reason}.',
|
|
63
|
+
hostCapacityUnknown: 'The Host concurrent-subagent ceiling is currently unavailable. Until it can be read, saving session limits, enabling a strategy and saving an enabled strategy are blocked; read-only viewing, closing a strategy, saving a non-enabled strategy, and pool or note edits remain available.',
|
|
64
|
+
hostCapacityReason: 'Host reason: {reason}',
|
|
65
|
+
hostCapacityUnknownBlocked: 'The Host concurrent-subagent ceiling is currently unavailable, so this action is blocked. Refresh after the Host can report it; read-only viewing, closing, saving a non-enabled strategy, and pool or note edits remain available.',
|
|
66
|
+
hostCapacityZero: 'The Host reports a concurrent-subagent ceiling of 0; a session limit cannot be saved until the Host reports a positive ceiling.',
|
|
67
|
+
hostLimitBlocked: 'The saved session limits (depth {depth} · concurrency {active}) exceed the current Host ceiling (depth {ceilingDepth} · concurrency {ceilingActive}). Adjust and save the limits on the model pool page first.',
|
|
68
|
+
maxDepth: 'Maximum recursion depth', maxActive: 'Concurrent subagents',
|
|
69
|
+
depthHint: '0–{max}; 0 disables further delegation', activeHint: '1–{max} (Host ceiling {host})', activeHintUnknown: 'Unavailable while the Host concurrent-subagent ceiling cannot be read.',
|
|
70
|
+
savedLimits: 'Saved: depth {depth} · concurrency {active}', noSavedLimits: 'No session limits saved yet.',
|
|
71
|
+
effectiveActive: 'Upper bound while enabled: depth {depth} · concurrency {active} (session limit {local}; Host ceiling {host}).',
|
|
72
|
+
effectiveUnknown: 'Upper bound while enabled: depth {depth} · concurrency {active}; the Host concurrent-subagent ceiling is currently unknown and cannot be verified. The Host still enforces its own capacity.',
|
|
73
|
+
effectiveClosed: 'Currently closed: session limits do not intervene; only Host limits apply.', limitsDirty: 'Unsaved limit changes; they take effect only after saving.',
|
|
74
|
+
saveLimits: 'Save session limits', discardLimits: 'Discard limit changes', limitError: 'Correct the marked limits before saving.',
|
|
75
|
+
errDepth: 'Depth must be an integer from 0 through {max}.', errActive: 'Concurrent subagents must be an integer from 1 through {max}.',
|
|
76
|
+
hostConflict: 'Saved session limits (depth {depth} · concurrency {active}) exceed the current Host ceiling (depth {ceilingDepth} · concurrency {ceilingActive}). New enable requests are rejected; enabled content is not stopped automatically. Adjust and save the limits on the model pool page.',
|
|
77
|
+
hostConflictActive: ' Enabled content is not stopped or reverted automatically.',
|
|
78
|
+
defaultTitle: 'New-session default', defaultLabelOn: 'New-session default: set', defaultLabelOff: 'New-session default: not set',
|
|
79
|
+
noDefault: 'Choose a strategy in the strategy pool.',
|
|
80
|
+
defaultSummary: 'Set to “{name}” · {type}', defaultLimitsInline: ' · depth {depth} · concurrency {active}',
|
|
81
|
+
defaultConflict: 'The limits stored with the default (depth {depth} · concurrency {active}) exceed the current Host ceiling (depth {ceilingDepth} · concurrency {ceilingActive}): adjust before use. The original values are kept and are not widened automatically.',
|
|
82
|
+
defaultLimitsUnknown: 'The stored concurrency limit cannot be verified while the Host concurrent-subagent ceiling is unavailable.',
|
|
83
|
+
warningNoDefaultRestore: 'Choose a strategy card and click “Set as new-session default” first.',
|
|
84
|
+
warningNoActiveRestore: 'Choose and enable a strategy in the strategy pool first.',
|
|
85
|
+
warningNeedsAdjust: '“{name}” references models outside the session pool; edit the strategy before enabling it. Not enabled.',
|
|
86
|
+
warningNoLegacy: 'Legacy follow strategies cannot be enabled here; save them as fixed or Agent-chosen.',
|
|
87
|
+
recordsTitle: 'Execution insights', recordsSub: 'Real subagent creations recorded by this plugin; no sample tasks and no statistics.',
|
|
88
|
+
recordsHint: 'Records keep the strategy name from creation, even if the strategy is later renamed or deleted.',
|
|
89
|
+
noRecords: 'No execution insights yet.', task: 'Task', status: 'Status', statusAdmitted: 'Admitted', statusStarted: 'Started', statusDisposed: 'Released',
|
|
90
|
+
requested: 'Requested model', actual: 'Actual model', evidence: 'Evidence', recordedAt: 'Recorded at', noRequest: 'Not recorded', noActual: 'Not confirmed',
|
|
91
|
+
evidencePending: 'pending (no response yet)', evidenceRequested: 'request/header (no response yet)', legacyStrategyName: 'Legacy session strategy', legacyFollowName: 'Legacy follow strategy',
|
|
92
|
+
savedStrategy: 'Saved “{name}”. After enabling, it takes effect on the next main-Agent model call.', savedActive: 'Saved; it takes effect on the next main-Agent model call.',
|
|
93
|
+
deletedNotice: 'Deleted “{name}”.',
|
|
94
|
+
poolSaved: 'Model pool saved: {count} selected; it takes effect on the next main-Agent model call.',
|
|
95
|
+
poolAffected: 'Models referenced by {names} are no longer in the session pool; adjust them before enabling.',
|
|
96
|
+
poolNoChanges: 'No model-pool changes to save.',
|
|
97
|
+
limitsSaved: 'Session limits saved: depth {depth} · concurrency {active}; it takes effect on the next main-Agent model call.',
|
|
98
|
+
noChanges: 'No changes to save.', draftPreserved: 'Server state refreshed; unsaved drafts were preserved.', refreshed: 'Server state refreshed.', readCancelled: 'Reading cancelled; the panel will read again when reopened.',
|
|
99
|
+
writeCancelled: 'Waiting cancelled; the Host may have applied the write. Refresh to confirm.',
|
|
100
|
+
deleteConfirm: 'Delete “{name}”? This does not affect the current session.',
|
|
101
|
+
};
|
|
102
|
+
const zh = {
|
|
103
|
+
title: '子智能体模型选择策略', open: '打开子智能体模型选择策略', description: '当前会话内的策略、候选池与并发递归限制。',
|
|
104
|
+
scope: '当前会话', loading: '正在读取…', saving: '正在保存…', refreshing: '正在刷新…', busy: '处理中…',
|
|
105
|
+
refresh: '刷新服务端状态', cancel: '取消等待', retry: '重试读取', error: '请求失败', serverError: '宿主未返回结果。',
|
|
106
|
+
noModelsSelected: '未选择模型', badResponse: '宿主返回的策略快照不符合约定。', missingCommand: '宿主未识别 /subagent-policy 命令。', staleResponse: '已拒绝属于其他会话的响应。',
|
|
107
|
+
conflict: '版本冲突', conflictHelp: '服务端已有新版本,本地草稿已保留。载入服务端版本后可继续。', loadServer: '载入服务端版本', mustRefresh: '再次写入前需要先刷新确认。',
|
|
108
|
+
readOnly: '当前会话无权编辑策略,继承的快照为只读。', noSession: '请选择会话后查看策略。', noSnapshot: '尚未读取策略快照。',
|
|
109
|
+
navLabel: '策略管理页面', pageStrategies: '选择策略', pageModels: '模型池', pageRecords: '执行洞察',
|
|
110
|
+
sessionTitle: '策略开关', policyLabelOn: '策略开关:已启用', policyLabelOff: '策略开关:未启用',
|
|
111
|
+
closedSummary: '未启用;按宿主默认(通常跟随父模型)。',
|
|
112
|
+
activeLine: '已启用「{name}」· {type} · {target}', targetFixed: '{name}', targetAuto: '{count} 个候选',
|
|
113
|
+
strategies: '策略池', countSuffix: '{count} 份', newStrategy: '新增策略', emptyStrategies: '暂无策略。可新增一份。',
|
|
114
|
+
activeBadge: '当前启用', defaultBadge: '新会话默认', defaultStaleBadge: '新会话默认(来源未更新)', adjustBadge: '需调整', stateOff: '未启用',
|
|
115
|
+
legacyBadge: '旧版跟随(只读)', legacyHint: '该旧版策略跟随父模型;保存为固定或 Agent 自选即为显式转换。',
|
|
116
|
+
edit: '编辑', editLabel: '编辑策略:{name}', enable: '启用', enableLabel: '启用策略:{name}', disable: '停用', disableLabel: '停用策略:{name}',
|
|
117
|
+
setDefault: '设为新会话默认', setDefaultLabel: '设为新会话默认:{name}', delete: '删除', deleteLabel: '删除策略:{name}',
|
|
118
|
+
deleteActiveHint: '当前启用,删除前需先关闭。', deleteDefaultHint: '新会话默认来源,删除前需先更改或清除新会话默认。', needsAdjustHint: '引用的模型不在会话池,启用前需调整。',
|
|
119
|
+
newStrategyTitle: '新增策略', editStrategyTitle: '编辑策略', back: '← 返回策略列表', backModels: '← 返回模型池', unsavedBadge: '未保存', savedBadge: '已保存',
|
|
120
|
+
detailSubNew: '保存后仍不会自动启用;启用后,在下一次主 Agent 发起模型调用时生效。',
|
|
121
|
+
detailSubSaved: '保存只更新策略库;启用后,在下一次主 Agent 发起模型调用时生效。',
|
|
122
|
+
detailSubActive: '当前会话启用中;保存后,在下一次主 Agent 发起模型调用时生效。',
|
|
123
|
+
convertedHint: '当前为旧版跟随策略;保存为固定或 Agent 自选会显式转换。',
|
|
124
|
+
nameLabel: '策略名称', namePlaceholder: '例如:实现默认', typeLabel: '策略类型', typeFixed: '固定模型', typeAuto: 'Agent 自选',
|
|
125
|
+
typeHintFixed: '委派时只使用选中的这一个模型,不做自动换选。', typeHintAuto: '从会话池勾选候选并写下选择说明;不做评分或静默回退。',
|
|
126
|
+
fixedPanel: '固定模型', fixedHint: '从已保存的会话池中单选一个模型。', autoPanel: '候选子集', autoHint: '从会话池勾选候选;Agent 自选只在这个子集内决策。',
|
|
127
|
+
noPoolFixed: '会话池为空,请先在「模型池」勾选并保存。', noPoolAuto: '会话池为空,请先在「模型池」勾选并保存。',
|
|
128
|
+
textLabel: '选择说明', textHint: '说明只供决策参考,不扩大授权。', textCount: '{count}/{max}',
|
|
129
|
+
saveStrategy: '保存策略', discard: '放弃修改', cancelEdit: '取消',
|
|
130
|
+
formError: '请修正标记的字段后再保存。', errNameEmpty: '请填写策略名称。', errNameLong: '策略名称过长。', errNameDup: '已有同名策略,请换一个名称。',
|
|
131
|
+
errFixed: '请选择一个固定模型。', errModels: '至少勾选一个候选模型。', errText: '选择说明过长。',
|
|
132
|
+
staleRef: '不在会话池', staleRefHint: '引用会保留;启用前需调整。',
|
|
133
|
+
modelsTitle: '模型池', modelsSub: '勾选当前会话允许作为策略候选的模型。', modelCount: '{count} 个候选',
|
|
134
|
+
selectAll: '全选', clearAll: '取消全选', selectedCount: '已选 {count} 个', savedCount: '已保存:{count} 个模型',
|
|
135
|
+
poolDirty: '有未保存的模型池修改;保存前不生效。', poolImpact: '模型池与已保存一致。',
|
|
136
|
+
poolImpactRemove: '保存后移除:{items}。', poolImpactProtect: '当前启用策略引用部分待移除模型,保存会被阻止。',
|
|
137
|
+
poolImpactAdd: '保存后加入:{items};不会自动加入任何策略。', poolError: '模型池未保存。',
|
|
138
|
+
poolBlockedActive: '无法保存:当前启用策略「{name}」仍引用{models}。请先在策略中调整该引用,或关闭当前策略后再移除。',
|
|
139
|
+
savePool: '保存模型池', discardPool: '放弃池修改', emptyModels: '当前会话没有可用候选。',
|
|
140
|
+
poolSync: '刷新候选', poolSyncConfirm: '将移除 {count} 个宿主当前未授权或不可用的候选,并同步清理策略中的失效引用;确定刷新?',
|
|
141
|
+
poolSynced: '已刷新候选池:移除 {count} 个失效项({names})。', poolSyncedNone: '候选池已是最新,无移除。',
|
|
142
|
+
poolToggleLabel: '模型池:{name}', abilityCard: '能力卡', abilityCardLabel: '打开能力卡:{name}({provider})', abilityTitle: '能力卡',
|
|
143
|
+
purposeLabel: '简短说明', noteLabel: '通用使用说明', noPurpose: '未填写用途',
|
|
144
|
+
noteHintUseCase: '只写使用场景与限制,不记录价格或性能结论。',
|
|
145
|
+
noteHintEffect: '仅保存说明:保存后,在下一次主 Agent 发起模型调用时生效。',
|
|
146
|
+
saveNote: '保存说明', discardNote: '放弃说明修改', noteSaved: '已保存能力卡,下次发起模型调用时生效。',
|
|
147
|
+
reasoningLine: '思考等级:{names}', reasoningDefault: '(默认 {name})', reasoningNone: '思考等级:—',
|
|
148
|
+
unavailableHint: '当前未获宿主授权或不可用,不能加入会话池。',
|
|
149
|
+
limitsTitle: '子智能体并发与递归', sharedBadge: '会话共用',
|
|
150
|
+
limitsSub: '整个会话共用;实际委派须同时满足宿主上限与本会话限制。',
|
|
151
|
+
hostCeiling: '宿主上限:最大递归深度 {depth};并发子智能体数量 {active}(已从宿主读取)。',
|
|
152
|
+
hostCeilingUnknown: '宿主深度上限:{depth}。当前无法读取或校验宿主并发子智能体数量上限。{reason}本插件不猜测该值,宿主仍独立执行自身容量限制。',
|
|
153
|
+
hostCeilingReason: ' 原因:{reason}。',
|
|
154
|
+
hostCapacityUnknown: '当前无法读取宿主并发子智能体数量上限。读取成功前,保存本会话限制、启用策略和保存启用中策略会被禁止;只读查看、关闭策略、保存非启用策略以及模型池或说明修改仍可使用。',
|
|
155
|
+
hostCapacityReason: '宿主原因:{reason}',
|
|
156
|
+
hostCapacityUnknownBlocked: '当前无法读取宿主并发子智能体数量上限,因此该操作被禁止。待宿主可报告后刷新;只读查看、关闭、保存非启用策略以及模型池或说明修改仍可使用。',
|
|
157
|
+
hostCapacityZero: '宿主报告并发子智能体数量上限为 0;在宿主报告正数上限前,无法保存本会话限制。',
|
|
158
|
+
hostLimitBlocked: '已保存的本会话限制(深度 {depth} · 并发 {active})超出当前宿主上限(深度 {ceilingDepth} · 并发 {ceilingActive})。请先到「模型池」调整并保存限制。',
|
|
159
|
+
maxDepth: '最大递归深度', maxActive: '并发子智能体数量',
|
|
160
|
+
depthHint: '0–{max};0 表示不再向下委派', activeHint: '1–{max}(宿主上限 {host})', activeHintUnknown: '当前无法读取宿主并发子智能体数量上限,暂不能保存本会话限制。',
|
|
161
|
+
savedLimits: '已保存:深度 {depth} · 并发 {active}', noSavedLimits: '尚未保存本会话限制。',
|
|
162
|
+
effectiveActive: '启用后有效上界:深度 {depth} · 并发 {active}(本会话限制 {local};宿主上限 {host})。',
|
|
163
|
+
effectiveUnknown: '启用后有效上界:深度 {depth} · 并发 {active};当前无法读取或校验宿主并发子智能体数量上限,宿主仍独立执行自身容量限制。',
|
|
164
|
+
effectiveClosed: '当前未启用:本会话附加限制不介入,仅宿主限制生效。', limitsDirty: '有未保存的限制修改;保存前不生效。',
|
|
165
|
+
saveLimits: '保存本会话限制', discardLimits: '放弃限制修改', limitError: '请修正标记的限制后再保存。',
|
|
166
|
+
errDepth: '最大递归深度需为 0–{max} 的整数。', errActive: '并发子智能体数量需为 1–{max} 的整数。',
|
|
167
|
+
hostConflict: '已保存的本会话限制(深度 {depth} · 并发 {active})超出当前宿主上限(深度 {ceilingDepth} · 并发 {ceilingActive}):新的启用会被拒绝;已启用内容不会自动停用。请到「模型池」调整并保存本会话限制。',
|
|
168
|
+
hostConflictActive: ' 已启用内容不会因此被自动停用或回退。',
|
|
169
|
+
defaultTitle: '新会话默认', defaultLabelOn: '新会话默认:已设置', defaultLabelOff: '新会话默认:未设置',
|
|
170
|
+
noDefault: '请在策略池中进行选择。',
|
|
171
|
+
defaultSummary: '已设置「{name}」· {type}', defaultLimitsInline: ' · 深度 {depth} · 并发 {active}',
|
|
172
|
+
defaultConflict: '默认内容保存的限制(深度 {depth} · 并发 {active})超出当前宿主上限(深度 {ceilingDepth} · 并发 {ceilingActive}):需调整后使用;原值保留,不会自动放宽。',
|
|
173
|
+
defaultLimitsUnknown: '当前无法读取宿主并发子智能体数量上限,默认内容的并发限制无法校验。',
|
|
174
|
+
warningNoDefaultRestore: '请先在策略卡片上点「设为新会话默认」。',
|
|
175
|
+
warningNoActiveRestore: '请先在策略池选择一份策略并启用。',
|
|
176
|
+
warningNeedsAdjust: '「{name}」引用了不在会话池的模型,需先编辑策略调整引用;未启用。',
|
|
177
|
+
warningNoLegacy: '旧版跟随策略不能在此启用;请保存为固定或 Agent 自选。',
|
|
178
|
+
recordsTitle: '执行洞察', recordsSub: '本插件记录的真实子智能体创建;不创建示例任务,不提供统计。',
|
|
179
|
+
recordsHint: '记录保留创建时的策略名,之后重命名或删除策略不会改写历史。',
|
|
180
|
+
noRecords: '暂无执行洞察。', task: '任务', status: '状态', statusAdmitted: '已准入', statusStarted: '执行中', statusDisposed: '已释放',
|
|
181
|
+
requested: '请求模型', actual: '实际模型', evidence: '证据', recordedAt: '记录时间', noRequest: '未记录', noActual: '待确认',
|
|
182
|
+
evidencePending: 'pending(尚无响应)', evidenceRequested: 'request/header(尚无响应)', legacyStrategyName: '旧版会话策略', legacyFollowName: '旧版跟随策略',
|
|
183
|
+
savedStrategy: '已保存「{name}」。启用后,下次发起模型调用时生效。', savedActive: '已保存,下次发起模型调用时生效。',
|
|
184
|
+
deletedNotice: '已删除「{name}」。',
|
|
185
|
+
poolSaved: '已保存模型池:已选 {count} 个;下次发起模型调用时生效。',
|
|
186
|
+
poolAffected: '{names}引用的模型已不在会话池,需调整后才能启用。',
|
|
187
|
+
poolNoChanges: '没有需要保存的模型池修改。',
|
|
188
|
+
limitsSaved: '已保存本会话限制:深度 {depth} · 并发 {active};下次发起模型调用时生效。',
|
|
189
|
+
noChanges: '没有需要保存的修改。', draftPreserved: '服务端状态已刷新,未保存草稿已保留。', refreshed: '服务端状态已刷新。', readCancelled: '已取消读取;重新打开面板时会再次读取。',
|
|
190
|
+
writeCancelled: '已取消等待;宿主可能已经完成写入。再次保存前,请先刷新确认。',
|
|
191
|
+
deleteConfirm: '删除「{name}」?不影响当前会话。',
|
|
192
|
+
};
|
|
193
|
+
const CSS = `
|
|
194
|
+
.lezsp{box-sizing:border-box;width:100%;height:100%;min-width:0;overflow:auto;overflow-wrap:anywhere;padding:14px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-module-platform);font:inherit;font-size:13px;line-height:1.55}
|
|
195
|
+
.lezsp *,.lezsp-header *{box-sizing:border-box}.lezsp [hidden]{display:none!important}
|
|
196
|
+
.lezsp h1{font-size:17px;line-height:1.3;margin:0}.lezsp h2{font-size:16px;line-height:1.4;margin:0}.lezsp h3{font-size:13px;margin:0;font-weight:600}
|
|
197
|
+
.lezsp p{margin:5px 0}.lezsp section{margin:12px 0}.lezsp small,.lezsp .lezsp-muted{color:var(--dsw-alias-label-tertiary);font-size:12px}
|
|
198
|
+
.lezsp-sub{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
|
199
|
+
.lezsp-nav{display:flex;gap:3px;margin-top:12px;padding:3px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
200
|
+
.lezsp-nav button{flex:1 1 0;min-width:0;padding:6px 3px;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:normal;overflow-wrap:anywhere}
|
|
201
|
+
.lezsp-nav button[aria-current="page"]{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}
|
|
202
|
+
.lezsp-status{margin:10px 0;padding:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);display:grid;gap:6px}
|
|
203
|
+
.lezsp-pill{display:inline-flex;align-items:center;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;font-size:11px;font-weight:700;white-space:nowrap}
|
|
204
|
+
.lezsp-pill.on{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-module-platform)}
|
|
205
|
+
.lezsp-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;min-width:0}.lezsp-between{justify-content:space-between}.lezsp-stack{display:grid;gap:10px;min-width:0}
|
|
206
|
+
.lezsp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(140px,100%),1fr));gap:10px;min-width:0}
|
|
207
|
+
.lezsp-card{padding:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;min-width:0;background:var(--dsw-alias-bg-layer-1)}
|
|
208
|
+
.lezsp-card.active{border-color:var(--dsw-alias-brand-primary)}
|
|
209
|
+
.lezsp-badges{display:flex;flex-wrap:wrap;gap:4px}.lezsp-badge{padding:1px 6px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;font-size:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}
|
|
210
|
+
.lezsp-badge.on{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-module-platform);font-weight:700}
|
|
211
|
+
.lezsp-badge.warn{border-color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary));color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary));font-weight:700}
|
|
212
|
+
.lezsp-meta{margin-top:3px;color:var(--dsw-alias-label-secondary);font-size:12px}.lezsp-meta strong{color:var(--dsw-alias-label-primary)}
|
|
213
|
+
.lezsp-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
|
|
214
|
+
.lezsp .lezsp-switch{position:relative;display:inline-flex;align-items:center;flex:none;cursor:pointer}
|
|
215
|
+
.lezsp .lezsp-seg{display:flex;gap:4px;margin:0 0 10px;padding:3px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
|
|
216
|
+
.lezsp .lezsp-seg button{flex:1 1 0;min-width:0;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-tertiary);font-weight:700}
|
|
217
|
+
.lezsp .lezsp-seg button[aria-pressed="true"]{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}
|
|
218
|
+
.lezsp input.lezsp-switch-input[type=checkbox]{position:absolute;width:1px;height:1px;opacity:0;margin:0}
|
|
219
|
+
.lezsp .lezsp-switch-track{position:relative;display:inline-block;width:34px;height:18px;border-radius:999px;background:var(--dsw-alias-border-l2);transition:background .15s}
|
|
220
|
+
.lezsp .lezsp-switch-track::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-bg-module-platform);box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
|
|
221
|
+
.lezsp .lezsp-switch input:checked+.lezsp-switch-track{background:var(--dsw-alias-brand-primary)}
|
|
222
|
+
.lezsp .lezsp-switch input:checked+.lezsp-switch-track::after{transform:translateX(16px)}
|
|
223
|
+
.lezsp .lezsp-switch input:focus-visible+.lezsp-switch-track{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
224
|
+
.lezsp .lezsp-card-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 6px;margin:0}
|
|
225
|
+
.lezsp .lezsp-card-name{font-size:14px;font-weight:700;line-height:1.4}
|
|
226
|
+
.lezsp .lezsp-card-models{margin:4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:11px}
|
|
227
|
+
.lezsp .lezsp-card-model-line{display:block}
|
|
228
|
+
.lezsp .lezsp-state{color:var(--dsw-alias-label-tertiary);font-size:12px}
|
|
229
|
+
.lezsp .lezsp-state.on{color:var(--dsw-alias-brand-primary);font-weight:700}
|
|
230
|
+
.lezsp .lezsp-state.warn{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary));font-weight:700}
|
|
231
|
+
.lezsp .lezsp-detail-meta{margin-left:6px;color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400}
|
|
232
|
+
.lezsp button,.lezsp-header button{font:inherit;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;white-space:normal;overflow-wrap:anywhere}
|
|
233
|
+
.lezsp button:hover:not(:disabled),.lezsp-header button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3)}
|
|
234
|
+
.lezsp button:disabled{opacity:.55;cursor:not-allowed}.lezsp .lezsp-primary{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-module-platform);font-weight:700}
|
|
235
|
+
.lezsp .lezsp-small{padding:4px 8px;font-size:11px}
|
|
236
|
+
.lezsp .lezsp-link{padding:2px 0;border:0;background:transparent;color:var(--dsw-alias-brand-primary);font-weight:600;text-decoration:underline;text-underline-offset:2px}
|
|
237
|
+
.lezsp :is(button,input,textarea,select,summary):focus-visible,.lezsp-header button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
238
|
+
.lezsp h1:focus-visible,.lezsp h2:focus-visible,.lezsp h3:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
239
|
+
.lezsp fieldset{padding:9px 10px;margin:0 0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;min-width:0}.lezsp legend{padding:0 4px;font-size:12px;font-weight:700}
|
|
240
|
+
.lezsp-field{display:grid;gap:4px;margin:0 0 11px;min-width:0}.lezsp-field>span:first-child{font-size:12px;font-weight:700}
|
|
241
|
+
.lezsp input:not([type=checkbox]):not([type=radio]),.lezsp select,.lezsp textarea{width:100%;min-width:0;max-width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;line-height:1.5}
|
|
242
|
+
.lezsp textarea{resize:vertical;min-height:62px;overflow-wrap:anywhere}
|
|
243
|
+
.lezsp input[type=checkbox],.lezsp input[type=radio]{width:16px;height:16px;flex:none;margin:2px 0;accent-color:var(--dsw-alias-brand-primary)}
|
|
244
|
+
.lezsp :is(input,textarea,select,fieldset)[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
|
|
245
|
+
.lezsp .lezsp-error{color:var(--dsw-alias-state-error-primary);font-size:11px;font-weight:700}
|
|
246
|
+
.lezsp .lezsp-banner{margin:9px 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-left:4px solid var(--dsw-alias-brand-primary);border-radius:8px;background:var(--dsw-alias-bg-layer-1);min-width:0}
|
|
247
|
+
.lezsp .lezsp-banner.warn{border-left-color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary))}
|
|
248
|
+
.lezsp .lezsp-banner.err{border-left-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
249
|
+
.lezsp .lezsp-fold{padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);min-width:0}
|
|
250
|
+
.lezsp .lezsp-fold>summary{cursor:pointer;font-size:12px;font-weight:700}.lezsp .lezsp-fold>div,.lezsp .lezsp-fold>dl{margin-top:8px}
|
|
251
|
+
.lezsp dl{display:grid;grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:3px 10px;margin:8px 0}.lezsp dt{color:var(--dsw-alias-label-tertiary)}.lezsp dd{margin:0;min-width:0;overflow-wrap:anywhere}
|
|
252
|
+
.lezsp ul,.lezsp ol{padding-left:20px;margin:6px 0}.lezsp .lezsp-route{font-family:ui-monospace,monospace;font-size:11px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
253
|
+
.lezsp pre{max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;font-family:ui-monospace,monospace;font-size:11px;margin:8px 0}
|
|
254
|
+
.lezsp .lezsp-model-choice{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;padding:7px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);cursor:pointer;min-width:0}
|
|
255
|
+
.lezsp .lezsp-model-choice.checked{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
256
|
+
.lezsp .lezsp-model-choice.stale{border-color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary))}
|
|
257
|
+
.lezsp .lezsp-choice-body{display:grid;gap:1px;min-width:0}.lezsp .lezsp-choice-name{font-size:12px;font-weight:700}
|
|
258
|
+
.lezsp .lezsp-pool-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:start;padding:8px 9px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);min-width:0}
|
|
259
|
+
.lezsp .lezsp-pool-row.stale{border-color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary))}
|
|
260
|
+
.lezsp .lezsp-empty{padding:14px;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}
|
|
261
|
+
.lezsp .lezsp-record{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);min-width:0}
|
|
262
|
+
.lezsp .lezsp-record summary{padding:9px 10px;cursor:pointer;list-style-position:inside}.lezsp .lezsp-record-body{padding:0 10px 10px;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
263
|
+
.lezsp .lezsp-record-head{display:flex;flex-wrap:wrap;gap:3px 8px;align-items:baseline}.lezsp .lezsp-time{color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
|
|
264
|
+
.lezsp .lezsp-status-tag{padding:0 6px;border:1px solid currentColor;border-radius:999px;font-size:11px;font-weight:700;white-space:nowrap;color:var(--dsw-alias-label-secondary)}
|
|
265
|
+
.lezsp .lezsp-form-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
266
|
+
.lezsp .lezsp-section-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin:14px 0 8px}
|
|
267
|
+
.lezsp .lezsp-count{color:var(--dsw-alias-label-tertiary);font-weight:400;font-size:12px}
|
|
268
|
+
.lezsp .lezsp-detail-title{margin-top:8px;font-size:16px;line-height:1.35}.lezsp .lezsp-note{margin:9px 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-left:4px solid var(--dsw-alias-brand-primary);border-radius:8px;background:var(--dsw-alias-bg-layer-1);font-size:12px}
|
|
269
|
+
.lezsp .lezsp-note.warn{border-left-color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-primary))}
|
|
270
|
+
.lezsp .lezsp-session-chip{display:inline-flex;align-items:center;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;white-space:nowrap}
|
|
271
|
+
.lezsp-header{display:inline-flex;gap:4px;align-items:center;min-width:0}.lezsp-header button{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:4px;border:0;background:transparent;color:var(--dsw-alias-label-secondary)}
|
|
272
|
+
@media(max-width:420px){.lezsp{padding:10px}.lezsp-nav button{font-size:11px}.lezsp dl{grid-template-columns:1fr;gap:1px}.lezsp dd{margin-bottom:6px}.lezsp-pool-row{grid-template-columns:auto minmax(0,1fr)}}
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
// @lez-pure-start
|
|
276
|
+
const keyOf = (route) => JSON.stringify([route && route.provider, route && route.model]);
|
|
277
|
+
const routeLabel = (route) => route && route.provider && route.model ? route.provider + '/' + route.model : '';
|
|
278
|
+
const sameRoute = (left, right) => !!left && !!right && left.provider === right.provider && left.model === right.model;
|
|
279
|
+
const sameRouteSet = (left, right) => {
|
|
280
|
+
const a = left instanceof Set ? left : new Set(left || []);
|
|
281
|
+
const b = right instanceof Set ? right : new Set(right || []);
|
|
282
|
+
if (a.size !== b.size) return false;
|
|
283
|
+
for (const key of a) if (!b.has(key)) return false;
|
|
284
|
+
return true;
|
|
285
|
+
};
|
|
286
|
+
const isStale = (generation, serial, alive) => !alive || generation !== serial;
|
|
287
|
+
const PLUGIN_MAX_ACTIVE = 32;
|
|
288
|
+
function hostCapacity(host) {
|
|
289
|
+
const raw = host && Number.isSafeInteger(host.maxActive) ? host.maxActive : null;
|
|
290
|
+
const known = Boolean(host && host.maxActiveKnown === true && raw !== null && raw >= 0);
|
|
291
|
+
const reason = host && typeof host.maxActiveReason === 'string' ? host.maxActiveReason.trim() : '';
|
|
292
|
+
const ceiling = known ? Math.min(PLUGIN_MAX_ACTIVE, raw) : PLUGIN_MAX_ACTIVE;
|
|
293
|
+
return {known, raw: known ? raw : null, ceiling, reason, canSave: known && ceiling >= 1};
|
|
294
|
+
}
|
|
295
|
+
function strategyDraftOf(strategy) {
|
|
296
|
+
const type = strategy ? (strategy.type === 'inherit' ? 'fixed' : strategy.type) : 'fixed';
|
|
297
|
+
const allModels = new Set((strategy && strategy.models || []).map(keyOf));
|
|
298
|
+
const fixedRoute = strategy && strategy.type === 'fixed' ? keyOf({ provider: strategy.fixedProvider, model: strategy.fixedModel }) : (allModels.size ? [...allModels][0] : '');
|
|
299
|
+
const models = type === 'fixed' ? new Set(fixedRoute ? [fixedRoute] : []) : allModels;
|
|
300
|
+
return { id: strategy && strategy.id ? strategy.id : null, name: strategy && strategy.name ? strategy.name : '', type, models, fixedRoute, strategyText: strategy && strategy.strategyText ? strategy.strategyText : '', dirty: false, legacy: Boolean(strategy && strategy.type === 'inherit'), converting: Boolean(strategy && strategy.type === 'inherit') };
|
|
301
|
+
}
|
|
302
|
+
function assertSnapshotShape(snap, sessionId) { if (!snap || typeof snap !== 'object' || snap.sessionId !== sessionId) {
|
|
303
|
+
const error = new Error(snap && snap.sessionId ? 'stale-session' : 'bad-snapshot');
|
|
304
|
+
error.code = snap && snap.sessionId ? 'staleResponse' : 'badResponse';
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
const whole = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
308
|
+
// Reasoning metadata is optional so older Host snapshots stay readable.
|
|
309
|
+
const validReasoning = (value) => value === undefined || value === null || (
|
|
310
|
+
typeof value === 'object' && Array.isArray(value.efforts)
|
|
311
|
+
&& (value.defaultEffort === undefined || typeof value.defaultEffort === 'string')
|
|
312
|
+
&& value.efforts.every((effort) => effort && typeof effort.id === 'string' && typeof effort.name === 'string'));
|
|
313
|
+
if (!whole(snap.revision) || typeof snap.canEdit !== 'boolean' || typeof snap.preview !== 'string') throw new Error('bad-snapshot');
|
|
314
|
+
const policy = snap.policy;
|
|
315
|
+
if (!policy || typeof policy.enabled !== 'boolean' || !['inherit', 'fixed', 'auto'].includes(policy.mode) || !Array.isArray(policy.models)) throw new Error('bad-snapshot');
|
|
316
|
+
const management = snap.management;
|
|
317
|
+
if (!management || typeof management !== 'object' || !Array.isArray(management.strategies) || !Array.isArray(management.pool)) throw new Error('bad-snapshot');
|
|
318
|
+
for (const strategy of management.strategies) {
|
|
319
|
+
if (!strategy || typeof strategy.id !== 'string' || typeof strategy.name !== 'string'
|
|
320
|
+
|| !['fixed', 'auto', 'inherit'].includes(strategy.type) || !Number.isSafeInteger(strategy.version)
|
|
321
|
+
|| !Array.isArray(strategy.models) || !strategy.models.every((route) => route && typeof route.provider === 'string' && typeof route.model === 'string')) throw new Error('bad-snapshot');
|
|
322
|
+
}
|
|
323
|
+
if (management.limits !== null && management.limits !== undefined) {
|
|
324
|
+
if (!whole(management.limits.maxDepth) || !Number.isSafeInteger(management.limits.maxActive)) throw new Error('bad-snapshot');
|
|
325
|
+
}
|
|
326
|
+
if (management.active !== null && management.active !== undefined) {
|
|
327
|
+
const active = management.active;
|
|
328
|
+
if (!active || typeof active.strategyId !== 'string' || !active.snapshot || !Array.isArray(active.snapshot.models)) throw new Error('bad-snapshot');
|
|
329
|
+
}
|
|
330
|
+
const fallback = snap.defaultSetting;
|
|
331
|
+
if (!fallback || !whole(fallback.revision) || !(fallback.snapshot === null || typeof fallback.snapshot === 'object')) throw new Error('bad-snapshot');
|
|
332
|
+
if (fallback.snapshot) {
|
|
333
|
+
if (typeof fallback.snapshot.name !== 'string' || !['fixed', 'auto'].includes(fallback.snapshot.type) || !Array.isArray(fallback.snapshot.models)) throw new Error('bad-snapshot');
|
|
334
|
+
}
|
|
335
|
+
if (!snap.host || !whole(snap.host.maxDepth) || !(snap.host.maxActive === null || Number.isSafeInteger(snap.host.maxActive)) || typeof snap.host.maxActiveKnown !== 'boolean') throw new Error('bad-snapshot');
|
|
336
|
+
if (snap.host.maxActiveKnown ? !Number.isSafeInteger(snap.host.maxActive) : snap.host.maxActive !== null) throw new Error('bad-snapshot');
|
|
337
|
+
if (snap.host.maxActiveReason !== undefined && snap.host.maxActiveReason !== null && typeof snap.host.maxActiveReason !== 'string') throw new Error('bad-snapshot');
|
|
338
|
+
if (!Array.isArray(snap.candidates) || !Array.isArray(snap.runs) || !Array.isArray(snap.diagnostics) || !Array.isArray(snap.coverage)) throw new Error('bad-snapshot');
|
|
339
|
+
for (const item of snap.candidates) {
|
|
340
|
+
if (!item || typeof item.provider !== 'string' || typeof item.model !== 'string' || typeof item.authorized !== 'boolean' || typeof item.available !== 'boolean' || !validReasoning(item.reasoning)) throw new Error('bad-snapshot');
|
|
341
|
+
}
|
|
342
|
+
return snap;
|
|
343
|
+
}
|
|
344
|
+
// @lez-pure-end
|
|
345
|
+
|
|
346
|
+
function PolicyIcon({ size = 20 } = {}) {
|
|
347
|
+
return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
348
|
+
h('rect', { x: 8, y: 2.5, width: 8, height: 5, rx: 1.5 }),
|
|
349
|
+
h('path', { d: 'M12 7.5v4m-7 4v-4h14v4' }),
|
|
350
|
+
h('rect', { x: 1.5, y: 15.5, width: 7, height: 5, rx: 1.5 }),
|
|
351
|
+
h('rect', { x: 15.5, y: 15.5, width: 7, height: 5, rx: 1.5 }));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
inject: ['slots', 'locale', 'remote', 'remote.commands', 'sidebarRight', 'sidebarRightTabs'],
|
|
356
|
+
apply(ctx) {
|
|
357
|
+
const translate = ctx.locale.bind(NS);
|
|
358
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), `${ID}: dictionaries`);
|
|
359
|
+
const fmt = (text, values = {}) => String(text).replace(/\{(\w+)\}/g, (match, key) => values[key] === undefined ? match : String(values[key]));
|
|
360
|
+
|
|
361
|
+
function PolicySession({ sessionId, t, visible, tabSignal }) {
|
|
362
|
+
const prefix = useId();
|
|
363
|
+
const [ui, setUi] = useState({ snap: null, page: 'strategies', detail: null, poolDraft: null, limitDraft: null, strategyDraft: null, modelDraft: null, busy: null, error: null, notice: null, noticeKind: '', conflict: false, mustRefresh: false, lastActiveId: null, lastDefault: null });
|
|
364
|
+
const uiRef = useRef(ui);
|
|
365
|
+
const operation = useRef(null);
|
|
366
|
+
const serial = useRef(0);
|
|
367
|
+
const alive = useRef(false);
|
|
368
|
+
const attempted = useRef(false);
|
|
369
|
+
const wasVisible = useRef(visible);
|
|
370
|
+
const commit = useCallback((update) => {
|
|
371
|
+
const next = typeof update === 'function' ? update(uiRef.current) : update;
|
|
372
|
+
uiRef.current = next;
|
|
373
|
+
setUi(next);
|
|
374
|
+
}, []);
|
|
375
|
+
const focusEl = (element) => { if (element && typeof element.focus === 'function') { try { element.focus({ preventScroll: true }); } catch { element.focus(); } } };
|
|
376
|
+
const idFor = (name) => `${prefix}-${name}`;
|
|
377
|
+
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
alive.current = true;
|
|
380
|
+
const stop = () => {
|
|
381
|
+
const active = operation.current;
|
|
382
|
+
serial.current += 1;
|
|
383
|
+
active?.controller.abort();
|
|
384
|
+
operation.current = null;
|
|
385
|
+
if (!uiRef.current.snap) attempted.current = false;
|
|
386
|
+
// An abort rejects the pending request, so its catch returns without
|
|
387
|
+
// clearing busy: release the gate here or the panel stays disabled.
|
|
388
|
+
if (active && alive.current) {
|
|
389
|
+
commit((state) => ({
|
|
390
|
+
...state,
|
|
391
|
+
busy: null,
|
|
392
|
+
mustRefresh: state.mustRefresh || active.kind !== 'read',
|
|
393
|
+
notice: t(active.kind === 'read' ? 'readCancelled' : 'writeCancelled'),
|
|
394
|
+
noticeKind: '',
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
tabSignal?.addEventListener('abort', stop, { once: true });
|
|
399
|
+
return () => {
|
|
400
|
+
alive.current = false;
|
|
401
|
+
stop();
|
|
402
|
+
tabSignal?.removeEventListener('abort', stop);
|
|
403
|
+
};
|
|
404
|
+
}, [tabSignal]);
|
|
405
|
+
|
|
406
|
+
const draftsFrom = useCallback((snap) => {
|
|
407
|
+
if (!snap) return { poolDraft: null, limitDraft: null, strategyDraft: null, modelDraft: null, detail: null };
|
|
408
|
+
const entries = new Map(snap.management.pool.map((entry) => [keyOf(entry), { provider: entry.provider, model: entry.model, purpose: entry.purpose || '', note: entry.note || '' }]));
|
|
409
|
+
const limits = snap.management.limits;
|
|
410
|
+
return {
|
|
411
|
+
poolDraft: { keys: new Set(entries.keys()), entries },
|
|
412
|
+
limitDraft: { maxDepth: limits ? String(limits.maxDepth) : '', maxActive: limits ? String(limits.maxActive) : '' },
|
|
413
|
+
strategyDraft: null, modelDraft: null, detail: null,
|
|
414
|
+
};
|
|
415
|
+
}, []);
|
|
416
|
+
const dirtyOf = (state) => {
|
|
417
|
+
const snap = state.snap;
|
|
418
|
+
if (!snap) return false;
|
|
419
|
+
if (state.poolDraft && !sameRouteSet(state.poolDraft.keys, new Set(snap.management.pool.map(keyOf)))) return true;
|
|
420
|
+
const limits = snap.management.limits;
|
|
421
|
+
if (state.limitDraft && (state.limitDraft.maxDepth !== (limits ? String(limits.maxDepth) : '') || state.limitDraft.maxActive !== (limits ? String(limits.maxActive) : ''))) return true;
|
|
422
|
+
if (state.strategyDraft?.dirty) return true;
|
|
423
|
+
if (state.modelDraft?.dirty) return true;
|
|
424
|
+
return false;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
function noticeFor(key, snap, state, reset) {
|
|
428
|
+
const values = {};
|
|
429
|
+
if (key === 'savedStrategy') values.name = state.strategyDraft?.name?.trim() || reset.name || '';
|
|
430
|
+
else if (key === 'poolSaved') values.count = snap.management.pool.length;
|
|
431
|
+
else if (key === 'limitsSaved') { values.depth = snap.management.limits?.maxDepth ?? ''; values.active = snap.management.limits?.maxActive ?? ''; }
|
|
432
|
+
else values.name = reset.name || (reset.id ? (snap.management.strategies.find((row) => row.id === reset.id)?.name || '') : '');
|
|
433
|
+
return fmt(t(key), values);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const request = useCallback(async (body, signal) => {
|
|
437
|
+
const reply = await ctx.remote.commands.execute(sessionId, '/subagent-policy ' + JSON.stringify(body), [], signal);
|
|
438
|
+
if (!reply?.ok) { const error = reply?.error || new Error(t('serverError')); error.transport = true; throw error; }
|
|
439
|
+
const outcome = reply.value?.result;
|
|
440
|
+
if (!outcome) throw new Error(t('missingCommand'));
|
|
441
|
+
if (outcome.kind !== 'success') {
|
|
442
|
+
let payload;
|
|
443
|
+
try { payload = JSON.parse(outcome.text); } catch { /* plain text */ }
|
|
444
|
+
const error = new Error(payload?.message || payload?.error?.message || outcome.text || t('error'));
|
|
445
|
+
error.code = payload?.code || '';
|
|
446
|
+
error.definite = true;
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
let snap;
|
|
450
|
+
try { snap = JSON.parse(outcome.text); } catch { throw new Error(t('badResponse')); }
|
|
451
|
+
try { return assertSnapshotShape(snap, sessionId); } catch (error) { throw new Error(t(error.code === 'staleResponse' ? 'staleResponse' : 'badResponse')); }
|
|
452
|
+
}, [sessionId, t]);
|
|
453
|
+
|
|
454
|
+
const perform = useCallback(async (kind, body, reset = {}) => {
|
|
455
|
+
if (!sessionId || operation.current || tabSignal?.aborted) return;
|
|
456
|
+
const generation = ++serial.current;
|
|
457
|
+
const controller = new AbortController();
|
|
458
|
+
operation.current = { controller, generation, kind };
|
|
459
|
+
const isCurrent = () => !isStale(generation, serial.current, alive.current) && !controller.signal.aborted && !tabSignal?.aborted;
|
|
460
|
+
commit((state) => ({ ...state, busy: kind, error: null, notice: null, noticeKind: '' }));
|
|
461
|
+
try {
|
|
462
|
+
const snap = await request(body || { action: 'read' }, controller.signal);
|
|
463
|
+
if (!isCurrent()) return;
|
|
464
|
+
commit((state) => {
|
|
465
|
+
const kept = dirtyOf(state);
|
|
466
|
+
const patch = {
|
|
467
|
+
snap, busy: null, conflict: false, mustRefresh: false, error: null, noticeKind: '',
|
|
468
|
+
lastActiveId: snap.management.active?.strategyId || state.lastActiveId || null,
|
|
469
|
+
lastDefault: snap.defaultSetting.snapshot || state.lastDefault || null,
|
|
470
|
+
};
|
|
471
|
+
if (kind === 'read') {
|
|
472
|
+
patch.notice = kept ? t('draftPreserved') : null;
|
|
473
|
+
return kept ? { ...state, ...patch } : { ...state, ...patch, ...draftsFrom(snap) };
|
|
474
|
+
}
|
|
475
|
+
let notice = '';
|
|
476
|
+
let noticeKind = '';
|
|
477
|
+
if (reset.notice === null) notice = null;
|
|
478
|
+
else {
|
|
479
|
+
const key = reset.notice || 'refreshed';
|
|
480
|
+
if ((key === 'savedStrategy' || key === 'savedActive') && state.snap && snap.revision === state.snap.revision) notice = t('noChanges');
|
|
481
|
+
else if (key === 'poolSaved') {
|
|
482
|
+
notice = fmt(t('poolSaved'), { count: snap.management.pool.length });
|
|
483
|
+
if (state.snap) {
|
|
484
|
+
const oldKeys = new Set(state.snap.management.pool.map(keyOf));
|
|
485
|
+
const newKeys = new Set(snap.management.pool.map(keyOf));
|
|
486
|
+
const removed = new Set([...oldKeys].filter((oldKey) => !newKeys.has(oldKey)));
|
|
487
|
+
const affected = snap.management.strategies.filter((strategy) => strategy.id !== snap.management.active?.strategyId
|
|
488
|
+
&& (strategy.models || []).some((route) => removed.has(keyOf(route))));
|
|
489
|
+
if (affected.length) {
|
|
490
|
+
notice += ' ' + fmt(t('poolAffected'), { names: affected.map((strategy) => `「${strategy.name}」`).join('、') });
|
|
491
|
+
noticeKind = 'warn';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} else notice = noticeFor(key, snap, state, reset);
|
|
495
|
+
}
|
|
496
|
+
patch.notice = notice;
|
|
497
|
+
patch.noticeKind = noticeKind;
|
|
498
|
+
if (reset.sync && state.snap) {
|
|
499
|
+
const oldSnap = state.snap;
|
|
500
|
+
const newPoolKeys = new Set(snap.management.pool.map(keyOf));
|
|
501
|
+
const removedPool = oldSnap.management.pool.filter((entry) => !newPoolKeys.has(keyOf(entry)));
|
|
502
|
+
const oldStrategyModels = new Map();
|
|
503
|
+
for (const strategy of oldSnap.management.strategies) for (const route of strategy.models || []) oldStrategyModels.set(keyOf(route), route);
|
|
504
|
+
const newStrategyKeys = new Set(snap.management.strategies.flatMap((strategy) => (strategy.models || []).map(keyOf)));
|
|
505
|
+
const removedRefs = [...oldStrategyModels.entries()].filter(([key]) => !newStrategyKeys.has(key)).map(([, route]) => route);
|
|
506
|
+
const changed = new Map();
|
|
507
|
+
for (const entry of [...removedPool, ...removedRefs]) {
|
|
508
|
+
const key = keyOf(entry);
|
|
509
|
+
if (!changed.has(key)) changed.set(key, entry);
|
|
510
|
+
}
|
|
511
|
+
patch.notice = changed.size
|
|
512
|
+
? fmt(t('poolSynced'), { count: changed.size, names: [...changed.values()].map((entry) => entry.name || entry.model).join('、') })
|
|
513
|
+
: t('poolSyncedNone');
|
|
514
|
+
patch.noticeKind = '';
|
|
515
|
+
}
|
|
516
|
+
if (!kept) return { ...state, ...patch, ...draftsFrom(snap) };
|
|
517
|
+
if (reset.strategy) {
|
|
518
|
+
const draft = state.strategyDraft;
|
|
519
|
+
const saved = (draft?.id ? snap.management.strategies.find((row) => row.id === draft.id) : null)
|
|
520
|
+
|| snap.management.strategies.find((row) => row.name === (draft?.name || '').trim());
|
|
521
|
+
patch.strategyDraft = saved ? strategyDraftOf(saved) : null;
|
|
522
|
+
patch.detail = saved ? { kind: 'strategy', id: saved.id } : null;
|
|
523
|
+
}
|
|
524
|
+
if (reset.pool) patch.poolDraft = draftsFrom(snap).poolDraft;
|
|
525
|
+
if (reset.model) {
|
|
526
|
+
const currentModel = state.modelDraft;
|
|
527
|
+
const entry = currentModel ? snap.management.pool.find((row) => sameRoute(row, currentModel)) : null;
|
|
528
|
+
patch.modelDraft = entry ? { ...entry, name: entry.name || entry.model, purpose: entry.purpose || '', note: entry.note || '', dirty: false } : null;
|
|
529
|
+
if (!entry) patch.detail = null;
|
|
530
|
+
else if (state.poolDraft) {
|
|
531
|
+
const entries = new Map(state.poolDraft.entries);
|
|
532
|
+
entries.set(keyOf(entry), { provider: entry.provider, model: entry.model, purpose: entry.purpose || '', note: entry.note || '' });
|
|
533
|
+
patch.poolDraft = { keys: new Set(state.poolDraft.keys), entries };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (reset.limits) patch.limitDraft = draftsFrom(snap).limitDraft;
|
|
537
|
+
return { ...state, ...patch };
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!isCurrent()) return;
|
|
541
|
+
const conflict = /conflict|revision|版本|冲突|其他位置更新/i.test(`${error?.code || ''} ${error?.message || ''}`);
|
|
542
|
+
commit((state) => ({
|
|
543
|
+
...state, busy: null,
|
|
544
|
+
error: { message: error?.message || String(error) },
|
|
545
|
+
conflict: state.conflict || conflict,
|
|
546
|
+
mustRefresh: !error?.definite && kind !== 'read' ? true : state.mustRefresh,
|
|
547
|
+
notice: error?.definite ? null : t(kind === 'read' ? 'readCancelled' : 'writeCancelled'),
|
|
548
|
+
noticeKind: '',
|
|
549
|
+
}));
|
|
550
|
+
} finally {
|
|
551
|
+
if (isCurrent()) { operation.current = null; commit((state) => ({ ...state, busy: null })); }
|
|
552
|
+
}
|
|
553
|
+
}, [sessionId, tabSignal, t, commit, request, draftsFrom]);
|
|
554
|
+
|
|
555
|
+
useEffect(() => {
|
|
556
|
+
const becameVisible = visible && !wasVisible.current;
|
|
557
|
+
wasVisible.current = visible;
|
|
558
|
+
if (!visible || operation.current) return;
|
|
559
|
+
if (uiRef.current.conflict || uiRef.current.mustRefresh) return;
|
|
560
|
+
if (!uiRef.current.snap) {
|
|
561
|
+
if (attempted.current) return;
|
|
562
|
+
attempted.current = true;
|
|
563
|
+
void perform('read');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (becameVisible) void perform('read');
|
|
567
|
+
}, [visible, perform]);
|
|
568
|
+
|
|
569
|
+
const read = () => void perform('read');
|
|
570
|
+
function capacityBlockMessage(action) {
|
|
571
|
+
if (!snap) return '';
|
|
572
|
+
if (!['strategy.enable', 'limits.save'].includes(action)) return '';
|
|
573
|
+
if (!capacity.known) return t('hostCapacityUnknownBlocked');
|
|
574
|
+
if (action === 'limits.save' && !capacity.canSave) return t('hostCapacityZero');
|
|
575
|
+
if (action === 'strategy.enable' && savedLimitsOver) {
|
|
576
|
+
const limits = snap.management.limits;
|
|
577
|
+
return fmt(t('hostLimitBlocked'), {
|
|
578
|
+
depth: limits.maxDepth, active: limits.maxActive,
|
|
579
|
+
ceilingDepth: snap.host.maxDepth, ceilingActive: capacity.raw,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return '';
|
|
583
|
+
}
|
|
584
|
+
const mutation = (action, extra, reset) => {
|
|
585
|
+
const snap = uiRef.current.snap;
|
|
586
|
+
if (!snap || !snap.canEdit || uiRef.current.conflict || uiRef.current.mustRefresh) return;
|
|
587
|
+
const blocked = capacityBlockMessage(action);
|
|
588
|
+
if (blocked) {
|
|
589
|
+
commit((state) => ({ ...state, error: { message: blocked }, notice: null }));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
void perform(action, { action, expectedRevision: snap.revision, ...extra }, reset);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const cancel = () => {
|
|
596
|
+
const active = operation.current;
|
|
597
|
+
if (!active) return;
|
|
598
|
+
serial.current += 1;
|
|
599
|
+
active.controller.abort();
|
|
600
|
+
operation.current = null;
|
|
601
|
+
commit((state) => ({ ...state, busy: null, mustRefresh: state.mustRefresh || active.kind !== 'read', notice: t(active.kind === 'read' ? 'readCancelled' : 'writeCancelled'), noticeKind: '' }));
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
function openStrategy(id) {
|
|
605
|
+
const snap = uiRef.current.snap;
|
|
606
|
+
const strategy = id ? snap.management.strategies.find((row) => row.id === id) : null;
|
|
607
|
+
if (id && !strategy) return;
|
|
608
|
+
commit((state) => ({ ...state, detail: { kind: 'strategy', id: strategy?.id || null }, strategyDraft: strategyDraftOf(strategy), error: null, notice: null, noticeKind: '' }));
|
|
609
|
+
}
|
|
610
|
+
function closeDetail() {
|
|
611
|
+
commit((state) => ({ ...state, detail: null, strategyDraft: null, modelDraft: null, error: null, notice: null, noticeKind: '' }));
|
|
612
|
+
}
|
|
613
|
+
function discardIfDirty() {
|
|
614
|
+
if (uiRef.current.strategyDraft?.dirty || uiRef.current.modelDraft?.dirty) return window.confirm(t('discard'));
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
function goPage(page) {
|
|
618
|
+
if (page === uiRef.current.page && !uiRef.current.detail) return;
|
|
619
|
+
if (!discardIfDirty()) return;
|
|
620
|
+
commit((state) => ({ ...state, page, detail: null, strategyDraft: null, modelDraft: null, error: null, notice: null, noticeKind: '' }));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function editStrategyField(field, value) {
|
|
624
|
+
commit((state) => state.strategyDraft ? { ...state, strategyDraft: { ...state.strategyDraft, [field]: value, dirty: true } } : state);
|
|
625
|
+
}
|
|
626
|
+
function switchType(type) {
|
|
627
|
+
commit((state) => {
|
|
628
|
+
const draft = state.strategyDraft;
|
|
629
|
+
if (!draft || draft.type === type) return state;
|
|
630
|
+
const models = new Set(draft.models);
|
|
631
|
+
let fixedRoute = draft.fixedRoute;
|
|
632
|
+
if (type === 'fixed') {
|
|
633
|
+
if (!fixedRoute || !models.has(fixedRoute)) fixedRoute = models.size ? [...models][0] : '';
|
|
634
|
+
models.clear();
|
|
635
|
+
if (fixedRoute) models.add(fixedRoute);
|
|
636
|
+
}
|
|
637
|
+
if (type === 'auto') fixedRoute = '';
|
|
638
|
+
return { ...state, strategyDraft: { ...draft, type, models, fixedRoute, dirty: true, converting: false } };
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function validateStrategyDraft() {
|
|
642
|
+
const snap = uiRef.current.snap;
|
|
643
|
+
const draft = uiRef.current.strategyDraft;
|
|
644
|
+
if (!snap || !draft) return {};
|
|
645
|
+
const errors = {};
|
|
646
|
+
const name = draft.name.trim();
|
|
647
|
+
if (!name) errors.name = t('errNameEmpty');
|
|
648
|
+
else if (name.length > 60) errors.name = t('errNameLong');
|
|
649
|
+
else if (snap.management.strategies.some((row) => row.id !== draft.id && row.name === name)) errors.name = t('errNameDup');
|
|
650
|
+
if (draft.type === 'fixed') { if (!draft.fixedRoute || draft.models.size !== 1 || !draft.models.has(draft.fixedRoute)) errors.fixed = t('errFixed'); }
|
|
651
|
+
else if (!draft.models.size) errors.models = t('errModels');
|
|
652
|
+
if (draft.strategyText.length > 2000) errors.text = t('errText');
|
|
653
|
+
return errors;
|
|
654
|
+
}
|
|
655
|
+
function strategyBody() {
|
|
656
|
+
const draft = uiRef.current.strategyDraft;
|
|
657
|
+
const fixed = draft.type === 'fixed' ? JSON.parse(draft.fixedRoute || '["",""]') : ['', ''];
|
|
658
|
+
const models = draft.type === 'fixed'
|
|
659
|
+
? (draft.fixedRoute ? [{ provider: fixed[0], model: fixed[1] }] : [])
|
|
660
|
+
: [...draft.models].map((key) => JSON.parse(key)).map(([provider, model]) => ({ provider, model }));
|
|
661
|
+
return { id: draft.id || undefined, name: draft.name.trim(), type: draft.type, models, fixedProvider: fixed[0], fixedModel: fixed[1], strategyText: draft.strategyText };
|
|
662
|
+
}
|
|
663
|
+
function saveStrategy() {
|
|
664
|
+
const errors = validateStrategyDraft();
|
|
665
|
+
if (Object.keys(errors).length) {
|
|
666
|
+
commit((state) => ({ ...state, error: { message: t('formError'), fields: errors } }));
|
|
667
|
+
const first = errors.name ? idFor('sName') : (errors.fixed || errors.models) ? idFor('sChoice') : idFor('sText');
|
|
668
|
+
focusEl(document.getElementById(first));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const current = uiRef.current;
|
|
672
|
+
const saved = current.strategyDraft?.id ? current.snap.management.strategies.find((row) => row.id === current.strategyDraft.id) : null;
|
|
673
|
+
const activeSnapshot = current.snap.management.active?.snapshot || null;
|
|
674
|
+
const isActive = Boolean(saved && current.snap.management.active?.strategyId === saved.id);
|
|
675
|
+
// A lagging legacy snapshot is aligned by the Host on a save even
|
|
676
|
+
// without field edits, so that case must still reach the Host.
|
|
677
|
+
const needsAlignment = Boolean(isActive && activeSnapshot && (activeSnapshot.version || 0) < (saved.version || 0));
|
|
678
|
+
if (!current.strategyDraft?.dirty && !needsAlignment) {
|
|
679
|
+
commit((state) => ({ ...state, error: null, notice: t('noChanges'), noticeKind: '' }));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (isActive) {
|
|
683
|
+
const blocked = capacityBlockMessage('strategy.enable');
|
|
684
|
+
if (blocked) { commit((state) => ({ ...state, error: { message: blocked }, notice: null })); return; }
|
|
685
|
+
}
|
|
686
|
+
mutation('strategy.save', { strategy: strategyBody() }, { strategy: true, notice: isActive ? 'savedActive' : 'savedStrategy' });
|
|
687
|
+
}
|
|
688
|
+
function loadServer() {
|
|
689
|
+
operation.current?.controller.abort();
|
|
690
|
+
operation.current = null;
|
|
691
|
+
serial.current += 1;
|
|
692
|
+
commit((state) => ({ ...state, conflict: false, error: null, mustRefresh: false, strategyDraft: null, modelDraft: null }));
|
|
693
|
+
void perform('read');
|
|
694
|
+
}
|
|
695
|
+
function openModel(key) {
|
|
696
|
+
const snap = uiRef.current.snap;
|
|
697
|
+
const entry = snap.management.pool.find((row) => keyOf(row) === key);
|
|
698
|
+
if (!entry) return;
|
|
699
|
+
const candidate = snap.candidates.find((row) => keyOf(row) === key);
|
|
700
|
+
const name = entry.name || candidate?.name || entry.model;
|
|
701
|
+
commit((state) => ({ ...state, detail: { kind: 'model', key }, modelDraft: { provider: entry.provider, model: entry.model, name, purpose: entry.purpose || '', note: entry.note || '', dirty: false }, error: null, notice: null, noticeKind: '' }));
|
|
702
|
+
}
|
|
703
|
+
function saveModelNote() {
|
|
704
|
+
const draft = uiRef.current.modelDraft;
|
|
705
|
+
if (!draft) return;
|
|
706
|
+
if (!draft.dirty) { commit((state) => ({ ...state, notice: t('noChanges') })); return; }
|
|
707
|
+
mutation('pool.note', { provider: draft.provider, model: draft.model, purpose: draft.purpose, note: draft.note }, { model: true, notice: 'noteSaved' });
|
|
708
|
+
}
|
|
709
|
+
function togglePool(route) {
|
|
710
|
+
commit((state) => {
|
|
711
|
+
const pool = state.poolDraft;
|
|
712
|
+
if (!pool) return state;
|
|
713
|
+
const keys = new Set(pool.keys); const entries = new Map(pool.entries);
|
|
714
|
+
const key = keyOf(route); const candidate = state.snap.candidates.find((row) => keyOf(row) === key);
|
|
715
|
+
if (keys.has(key)) keys.delete(key);
|
|
716
|
+
else {
|
|
717
|
+
if (!candidate?.authorized || !candidate?.available) return state;
|
|
718
|
+
if (keys.size >= 24) return state;
|
|
719
|
+
keys.add(key);
|
|
720
|
+
if (!entries.has(key)) entries.set(key, { provider: route.provider, model: route.model, purpose: (candidate.description || '').slice(0, 500), note: '' });
|
|
721
|
+
}
|
|
722
|
+
return { ...state, poolDraft: { keys, entries } };
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function selectAllPool() {
|
|
726
|
+
commit((state) => {
|
|
727
|
+
if (!state.poolDraft) return state;
|
|
728
|
+
const keys = new Set(state.poolDraft.keys); const entries = new Map(state.poolDraft.entries);
|
|
729
|
+
for (const candidate of state.snap.candidates) {
|
|
730
|
+
if (!candidate.authorized || !candidate.available) continue;
|
|
731
|
+
if (keys.size >= 24) break;
|
|
732
|
+
const key = keyOf(candidate); keys.add(key);
|
|
733
|
+
if (!entries.has(key)) entries.set(key, { provider: candidate.provider, model: candidate.model, purpose: (candidate.description || '').slice(0, 500), note: '' });
|
|
734
|
+
}
|
|
735
|
+
return { ...state, poolDraft: { keys, entries } };
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function clearAllPool() {
|
|
739
|
+
commit((state) => state.poolDraft ? { ...state, poolDraft: { keys: new Set(), entries: new Map(state.poolDraft.entries) } } : state);
|
|
740
|
+
}
|
|
741
|
+
function savePool() {
|
|
742
|
+
const snap = uiRef.current.snap; const pool = uiRef.current.poolDraft;
|
|
743
|
+
if (!snap || !pool) return;
|
|
744
|
+
const savedKeys = new Set(snap.management.pool.map(keyOf));
|
|
745
|
+
const removals = [...savedKeys].filter((key) => !pool.keys.has(key));
|
|
746
|
+
const additions = [...pool.keys].filter((key) => !savedKeys.has(key));
|
|
747
|
+
if (!removals.length && !additions.length) {
|
|
748
|
+
commit((state) => ({ ...state, error: null, notice: t('poolNoChanges'), noticeKind: '' }));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const active = snap.management.active || null;
|
|
752
|
+
const activeRoutes = new Set((active?.snapshot?.models || []).map(keyOf));
|
|
753
|
+
const activeStrategy = active ? snap.management.strategies.find((row) => row.id === active.strategyId) : null;
|
|
754
|
+
for (const route of activeStrategy?.models || []) activeRoutes.add(keyOf(route));
|
|
755
|
+
const protectedRemovals = removals.filter((key) => activeRoutes.has(key));
|
|
756
|
+
if (protectedRemovals.length) {
|
|
757
|
+
const names = protectedRemovals.map((key) => {
|
|
758
|
+
const entry = snap.management.pool.find((row) => keyOf(row) === key);
|
|
759
|
+
return entry?.name || JSON.parse(key)[1];
|
|
760
|
+
}).join('、');
|
|
761
|
+
commit((state) => ({ ...state, error: { message: fmt(t('poolBlockedActive'), { name: active?.snapshot?.name || activeStrategy?.name || '', models: names }) }, notice: null }));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const order = [...snap.candidates.map(keyOf), ...[...pool.keys].filter((key) => !snap.candidates.some((row) => keyOf(row) === key))];
|
|
765
|
+
const entries = order.filter((key) => pool.keys.has(key)).map((key) => pool.entries.get(key) || (() => { const [provider, model] = JSON.parse(key); return { provider, model, purpose: '', note: '' }; })());
|
|
766
|
+
mutation('pool.save', { pool: entries }, { pool: true, notice: 'poolSaved' });
|
|
767
|
+
}
|
|
768
|
+
function syncPool() {
|
|
769
|
+
const snap = uiRef.current.snap;
|
|
770
|
+
if (!snap) return;
|
|
771
|
+
// Removal candidates: pool entries plus strategy references the Host
|
|
772
|
+
// no longer authorizes or resolves (fixed routing targets and the
|
|
773
|
+
// enabled snapshot stay protected on the Host side).
|
|
774
|
+
const supportedKeys = new Set(snap.candidates.filter((row) => row.authorized && row.available).map(keyOf));
|
|
775
|
+
const inPool = new Set(snap.management.pool.map(keyOf));
|
|
776
|
+
const removable = snap.candidates.filter((row) => inPool.has(keyOf(row)) && (!row.authorized || !row.available));
|
|
777
|
+
const enabledKeys = new Set((snap.management.active?.snapshot?.models || []).map(keyOf));
|
|
778
|
+
const prunableKeys = new Set();
|
|
779
|
+
for (const strategy of snap.management.strategies) {
|
|
780
|
+
for (const route of strategy.models || []) {
|
|
781
|
+
const key = keyOf(route);
|
|
782
|
+
if (supportedKeys.has(key) || enabledKeys.has(key)) continue;
|
|
783
|
+
if (strategy.type === 'fixed' && strategy.fixedProvider === route.provider && strategy.fixedModel === route.model) continue;
|
|
784
|
+
prunableKeys.add(key);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const count = new Set([...removable.map(keyOf), ...prunableKeys]).size;
|
|
788
|
+
if (count && !window.confirm(fmt(t('poolSyncConfirm'), { count }))) return;
|
|
789
|
+
mutation('pool.sync', {}, { pool: true, notice: 'poolSynced', sync: true });
|
|
790
|
+
}
|
|
791
|
+
function saveLimits() {
|
|
792
|
+
const draft = uiRef.current.limitDraft; const snap = uiRef.current.snap;
|
|
793
|
+
if (!draft || !snap) return;
|
|
794
|
+
const blocked = capacityBlockMessage('limits.save');
|
|
795
|
+
if (blocked) {
|
|
796
|
+
commit((state) => ({ ...state, error: { message: blocked } }));
|
|
797
|
+
focusEl(document.getElementById(idFor('limActive')));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const depth = Number(draft.maxDepth); const active = Number(draft.maxActive);
|
|
801
|
+
const maxDepth = snap.host.maxDepth;
|
|
802
|
+
const maxActive = capacity.known ? capacity.ceiling : PLUGIN_MAX_ACTIVE;
|
|
803
|
+
if (!Number.isInteger(depth) || depth < 0 || depth > maxDepth) {
|
|
804
|
+
commit((state) => ({ ...state, error: { message: fmt(t('errDepth'), { max: maxDepth }) } }));
|
|
805
|
+
focusEl(document.getElementById(idFor('limDepth')));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (!Number.isInteger(active) || active < 1 || active > maxActive) {
|
|
809
|
+
commit((state) => ({ ...state, error: { message: fmt(t('errActive'), { max: maxActive }) } }));
|
|
810
|
+
focusEl(document.getElementById(idFor('limActive')));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
mutation('limits.save', { limits: { maxDepth: depth, maxActive: active } }, { limits: true, notice: 'limitsSaved' });
|
|
814
|
+
}
|
|
815
|
+
function warn(message) {
|
|
816
|
+
commit((state) => ({ ...state, error: null, notice: message, noticeKind: 'warn' }));
|
|
817
|
+
}
|
|
818
|
+
function closeActive() {
|
|
819
|
+
const current = uiRef.current.snap?.management.active;
|
|
820
|
+
if (!current) return;
|
|
821
|
+
commit((state) => ({ ...state, lastActiveId: current.strategyId }));
|
|
822
|
+
mutation('strategy.close', {}, { notice: null });
|
|
823
|
+
}
|
|
824
|
+
function restoreActive() {
|
|
825
|
+
const snap = uiRef.current.snap;
|
|
826
|
+
const id = uiRef.current.lastActiveId;
|
|
827
|
+
const target = id && snap ? snap.management.strategies.find((row) => row.id === id) : null;
|
|
828
|
+
if (!target) { warn(t('warningNoActiveRestore')); return; }
|
|
829
|
+
if (target.type === 'inherit') { warn(t('warningNoLegacy')); return; }
|
|
830
|
+
if (target.needsAdjust) { warn(fmt(t('warningNeedsAdjust'), { name: target.name })); return; }
|
|
831
|
+
const blocked = capacityBlockMessage('strategy.enable');
|
|
832
|
+
if (blocked) { warn(blocked); return; }
|
|
833
|
+
mutation('strategy.enable', { id: target.id }, { notice: null });
|
|
834
|
+
}
|
|
835
|
+
function clearDefault() {
|
|
836
|
+
const current = uiRef.current;
|
|
837
|
+
const snap = current.snap;
|
|
838
|
+
if (!snap || !snap.canEdit || current.conflict || current.mustRefresh) return;
|
|
839
|
+
if (snap.defaultSetting.snapshot && !current.lastDefault) {
|
|
840
|
+
commit((state) => ({ ...state, lastDefault: snap.defaultSetting.snapshot }));
|
|
841
|
+
}
|
|
842
|
+
// default.clear accepts only the default revision; the Host rejects
|
|
843
|
+
// an unexpected expectedRevision on this action.
|
|
844
|
+
void perform('default.clear', { action: 'default.clear', expectedDefaultRevision: snap.defaultSetting.revision }, { notice: null });
|
|
845
|
+
}
|
|
846
|
+
function toggleDefault(on) {
|
|
847
|
+
const snap = uiRef.current.snap;
|
|
848
|
+
if (!snap) return;
|
|
849
|
+
if (!on) {
|
|
850
|
+
if (!snap.defaultSetting.snapshot) return;
|
|
851
|
+
clearDefault();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (snap.defaultSetting.snapshot) return;
|
|
855
|
+
const remembered = uiRef.current.lastDefault;
|
|
856
|
+
const source = remembered?.sourceId ? snap.management.strategies.find((row) => row.id === remembered.sourceId) : null;
|
|
857
|
+
if (!source) { warn(t('warningNoDefaultRestore')); return; }
|
|
858
|
+
mutation('strategy.default', { expectedDefaultRevision: snap.defaultSetting.revision, id: source.id }, { notice: null });
|
|
859
|
+
}
|
|
860
|
+
function setDefault(id) {
|
|
861
|
+
const snap = uiRef.current.snap;
|
|
862
|
+
if (!snap) return;
|
|
863
|
+
mutation('strategy.default', { expectedDefaultRevision: snap.defaultSetting.revision, id }, { notice: null });
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const snap = ui.snap;
|
|
867
|
+
const page = ui.page;
|
|
868
|
+
const draft = ui.strategyDraft;
|
|
869
|
+
const busy = Boolean(ui.busy);
|
|
870
|
+
const capacity = hostCapacity(snap && snap.host);
|
|
871
|
+
const savedLimits = snap?.management?.limits || null;
|
|
872
|
+
const savedLimitsOver = Boolean(savedLimits && snap && (savedLimits.maxDepth > snap.host.maxDepth || (capacity.known && savedLimits.maxActive > capacity.raw)));
|
|
873
|
+
const editable = Boolean(snap?.canEdit) && !busy && !ui.conflict && !ui.mustRefresh;
|
|
874
|
+
const titleRefs = useRef({});
|
|
875
|
+
const titleRef = (name) => (element) => { if (element) titleRefs.current[name] = element; else delete titleRefs.current[name]; };
|
|
876
|
+
useEffect(() => {
|
|
877
|
+
if (!snap) return;
|
|
878
|
+
if (ui.detail?.kind === 'strategy') focusEl(titleRefs.current.strategyDetail);
|
|
879
|
+
else if (ui.detail?.kind === 'model') focusEl(titleRefs.current.modelDetail);
|
|
880
|
+
else focusEl(titleRefs.current[page]);
|
|
881
|
+
}, [page, ui.detail, snap?.revision]);
|
|
882
|
+
|
|
883
|
+
const button = (label, handler, options = {}) => h('button', { type: 'button', onClick: handler, disabled: options.disabled, className: options.className, title: options.title, 'aria-label': options.aria || label }, label);
|
|
884
|
+
const badge = (label, kind = '') => h('span', { className: `lezsp-badge ${kind}`.trim() }, label);
|
|
885
|
+
const banner = (content, kind = '') => h('div', { className: `lezsp-banner ${kind}`.trim(), role: kind === 'err' ? 'alert' : 'status' }, content);
|
|
886
|
+
const hostCapacityNotice = () => {
|
|
887
|
+
if (!snap || capacity.known) return null;
|
|
888
|
+
return h('div', { className: 'lezsp-banner warn', role: 'status' },
|
|
889
|
+
h('p', null, t('hostCapacityUnknown')),
|
|
890
|
+
capacity.reason ? h('p', { className: 'lezsp-muted' }, fmt(t('hostCapacityReason'), { reason: capacity.reason })) : null);
|
|
891
|
+
};
|
|
892
|
+
const formatTime = (value) => {
|
|
893
|
+
if (!value) return '—';
|
|
894
|
+
const date = new Date(value);
|
|
895
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
896
|
+
const pad = (number) => String(number).padStart(2, '0');
|
|
897
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
898
|
+
};
|
|
899
|
+
const typeLabel = (type) => type === 'fixed' ? t('typeFixed') : type === 'auto' ? t('typeAuto') : t('legacyBadge');
|
|
900
|
+
const strategyById = (id) => snap?.management.strategies.find((row) => row.id === id) || null;
|
|
901
|
+
const active = snap?.management.active || null;
|
|
902
|
+
const activeStrategy = active ? strategyById(active.strategyId) : null;
|
|
903
|
+
const activeTarget = active ? (active.snapshot.type === 'fixed'
|
|
904
|
+
? (() => { const info = (active.snapshot.modelsInfo || []).find((row) => row.provider === active.snapshot.fixedProvider && row.model === active.snapshot.fixedModel); return info?.name || active.snapshot.fixedModel; })()
|
|
905
|
+
: (active.snapshot.models || []).length) : null;
|
|
906
|
+
|
|
907
|
+
function modelName(route) {
|
|
908
|
+
const found = snap?.management.pool.find((row) => sameRoute(row, route)) || snap?.candidates.find((row) => sameRoute(row, route));
|
|
909
|
+
return found?.name || route?.model || '';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const switchControl = (suffix, checked, disabled, aria, onChange) => h('label', { className: 'lezsp-switch' },
|
|
913
|
+
h('input', { id: idFor(suffix), type: 'checkbox', role: 'switch', className: 'lezsp-switch-input', checked, disabled, 'aria-label': aria, onChange: (event) => onChange(event.target.checked) }),
|
|
914
|
+
h('span', { className: 'lezsp-switch-track', 'aria-hidden': true }));
|
|
915
|
+
|
|
916
|
+
const reasoningText = (candidate, entry) => {
|
|
917
|
+
const reasoning = candidate?.reasoning || entry?.reasoning || null;
|
|
918
|
+
const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts : [];
|
|
919
|
+
if (!efforts.length) return t('reasoningNone');
|
|
920
|
+
const names = efforts.map((effort) => effort.name || effort.id).join(' · ');
|
|
921
|
+
const current = reasoning.defaultEffort ? efforts.find((effort) => effort.id === reasoning.defaultEffort) : null;
|
|
922
|
+
return fmt(t('reasoningLine'), { names }) + (current ? fmt(t('reasoningDefault'), { name: current.name || current.id }) : '');
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
function strategyCard(strategy) {
|
|
926
|
+
const isActive = active?.strategyId === strategy.id;
|
|
927
|
+
const isDefault = snap.defaultSetting.snapshot?.sourceId === strategy.id;
|
|
928
|
+
const defaultStale = isDefault && (snap.defaultSetting.snapshot.version || 0) < (strategy.version || 1);
|
|
929
|
+
const adjust = Boolean(strategy.needsAdjust);
|
|
930
|
+
const byProvider = new Map();
|
|
931
|
+
for (const route of strategy.models || []) {
|
|
932
|
+
const name = modelName(route);
|
|
933
|
+
if (!byProvider.has(route.provider)) byProvider.set(route.provider, []);
|
|
934
|
+
byProvider.get(route.provider).push(name);
|
|
935
|
+
}
|
|
936
|
+
const modelLines = byProvider.size
|
|
937
|
+
? [...byProvider].map(([provider, names]) => `${provider}: ${names.join('、')}`)
|
|
938
|
+
: [t('noModelsSelected')];
|
|
939
|
+
const stateText = adjust ? t('adjustBadge') : isActive ? t('activeBadge') : t('stateOff');
|
|
940
|
+
const stateClass = `lezsp-state${adjust ? ' warn' : isActive ? ' on' : ''}`;
|
|
941
|
+
return h('article', { key: strategy.id, className: `lezsp-card${isActive ? ' active' : ''}`, 'aria-labelledby': idFor(`strategy-${strategy.id}`), tabIndex: -1 },
|
|
942
|
+
h('p', { className: 'lezsp-card-head' },
|
|
943
|
+
h('strong', { className: 'lezsp-card-name', id: idFor(`strategy-${strategy.id}`) }, strategy.name),
|
|
944
|
+
' · ', h('span', { className: 'lezsp-muted' }, typeLabel(strategy.type)), ' · ',
|
|
945
|
+
h('span', { className: stateClass }, stateText),
|
|
946
|
+
isDefault ? h(React.Fragment, null, ' · ', h('span', { className: defaultStale ? 'lezsp-state warn' : 'lezsp-muted' }, defaultStale ? t('defaultStaleBadge') : t('defaultBadge'))) : null),
|
|
947
|
+
h('p', { className: 'lezsp-card-models' }, modelLines.map((line) => h('span', { key: line, className: 'lezsp-card-model-line' }, line))),
|
|
948
|
+
adjust ? h('p', { className: 'lezsp-error' }, t('needsAdjustHint')) : null,
|
|
949
|
+
strategy.type === 'inherit' ? h('p', { className: 'lezsp-muted' }, t('legacyHint')) : null,
|
|
950
|
+
h('div', { className: 'lezsp-actions' },
|
|
951
|
+
button(t('edit'), () => openStrategy(strategy.id), { disabled: busy, aria: fmt(t('editLabel'), { name: strategy.name }) }),
|
|
952
|
+
isActive
|
|
953
|
+
? button(t('disable'), () => mutation('strategy.close', {}, { notice: null }), { disabled: !editable, aria: fmt(t('disableLabel'), { name: strategy.name }) })
|
|
954
|
+
: strategy.type === 'inherit' ? null : (() => {
|
|
955
|
+
const capacityReason = capacityBlockMessage('strategy.enable');
|
|
956
|
+
return button(t('enable'), () => mutation('strategy.enable', { id: strategy.id }, { notice: null }), { disabled: !editable || adjust || Boolean(capacityReason), title: capacityReason || undefined, aria: fmt(t('enableLabel'), { name: strategy.name }) });
|
|
957
|
+
})(),
|
|
958
|
+
button(t('setDefault'), () => setDefault(strategy.id), { disabled: !editable || strategy.type === 'inherit', aria: fmt(t('setDefaultLabel'), { name: strategy.name }) }),
|
|
959
|
+
button(t('delete'), () => {
|
|
960
|
+
if (!window.confirm(fmt(t('deleteConfirm'), { name: strategy.name }))) return;
|
|
961
|
+
mutation('strategy.delete', { id: strategy.id }, { notice: 'deletedNotice', name: strategy.name });
|
|
962
|
+
}, { disabled: !editable || isActive || isDefault, aria: fmt(t('deleteLabel'), { name: strategy.name }) })),
|
|
963
|
+
isActive ? h('p', { className: 'lezsp-muted' }, t('deleteActiveHint'))
|
|
964
|
+
: isDefault ? h('p', { className: 'lezsp-muted' }, t('deleteDefaultHint')) : null);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function strategiesPage() {
|
|
968
|
+
if (ui.detail?.kind === 'strategy' && draft) return strategyDetail();
|
|
969
|
+
const defaultValue = snap.defaultSetting.snapshot;
|
|
970
|
+
const valueLimits = defaultValue?.limits || null;
|
|
971
|
+
const defaultText = defaultValue
|
|
972
|
+
? fmt(t('defaultSummary'), { name: defaultValue.name, type: typeLabel(defaultValue.type) })
|
|
973
|
+
+ (valueLimits ? fmt(t('defaultLimitsInline'), { depth: valueLimits.maxDepth, active: valueLimits.maxActive }) : '')
|
|
974
|
+
: t('noDefault');
|
|
975
|
+
const defaultConflict = Boolean(valueLimits && (valueLimits.maxDepth > snap.host.maxDepth || (capacity.known && valueLimits.maxActive > capacity.raw)));
|
|
976
|
+
return h(React.Fragment, null,
|
|
977
|
+
h('section', { className: 'lezsp-status', 'aria-labelledby': idFor('sessionTitle') },
|
|
978
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
979
|
+
h('h2', { id: idFor('sessionTitle'), tabIndex: -1, ref: titleRef('strategies') }, t('sessionTitle')),
|
|
980
|
+
switchControl('policyToggle', Boolean(active), !editable, active ? t('policyLabelOn') : t('policyLabelOff'), (checked) => { if (checked) restoreActive(); else closeActive(); })),
|
|
981
|
+
h('p', null, active
|
|
982
|
+
? fmt(t('activeLine'), { name: active.snapshot.name, type: typeLabel(active.snapshot.type), target: active.snapshot.type === 'fixed' ? activeTarget : fmt(t('targetAuto'), { count: activeTarget }) })
|
|
983
|
+
: t('closedSummary'))),
|
|
984
|
+
h('section', { className: 'lezsp-status', 'aria-labelledby': idFor('defaultTitle') },
|
|
985
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
986
|
+
h('h2', { id: idFor('defaultTitle'), tabIndex: -1 }, t('defaultTitle')),
|
|
987
|
+
switchControl('defaultToggle', Boolean(defaultValue), !editable, defaultValue ? t('defaultLabelOn') : t('defaultLabelOff'), toggleDefault)),
|
|
988
|
+
h('p', null, defaultText),
|
|
989
|
+
defaultConflict ? banner(fmt(t('defaultConflict'), { depth: valueLimits.maxDepth, active: valueLimits.maxActive, ceilingDepth: snap.host.maxDepth, ceilingActive: capacity.known ? capacity.raw : '—' }), 'warn') : null),
|
|
990
|
+
hostCapacityNotice(),
|
|
991
|
+
savedLimitsOver && capacity.known ? banner(fmt(t('hostLimitBlocked'), { depth: savedLimits.maxDepth, active: savedLimits.maxActive, ceilingDepth: snap.host.maxDepth, ceilingActive: capacity.raw }) + (active ? t('hostConflictActive') : ''), 'warn') : null,
|
|
992
|
+
h('div', { className: 'lezsp-section-head' },
|
|
993
|
+
h('h2', { id: idFor('strategiesTitle'), tabIndex: -1 }, t('strategies'), ' ', h('span', { className: 'lezsp-count' }, fmt(t('countSuffix'), { count: snap.management.strategies.length }))),
|
|
994
|
+
button(t('newStrategy'), () => openStrategy(null), { className: 'lezsp-small lezsp-primary', disabled: !editable })),
|
|
995
|
+
snap.management.strategies.length
|
|
996
|
+
? h('div', { className: 'lezsp-stack' }, snap.management.strategies.map(strategyCard))
|
|
997
|
+
: h('div', { className: 'lezsp-empty' }, t('emptyStrategies')));
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function strategyDetail() {
|
|
1001
|
+
const errors = ui.error?.fields || {};
|
|
1002
|
+
const saved = draft.id ? strategyById(draft.id) : null;
|
|
1003
|
+
const isActive = Boolean(saved && active?.strategyId === saved.id);
|
|
1004
|
+
const saveBlocked = isActive ? capacityBlockMessage('strategy.enable') : '';
|
|
1005
|
+
const pool = snap.management.pool;
|
|
1006
|
+
const staleKeys = new Set([...draft.models].filter((key) => !pool.some((entry) => keyOf(entry) === key)));
|
|
1007
|
+
const fixedChoices = [...pool.map(keyOf), ...staleKeys];
|
|
1008
|
+
const autoChoices = [...pool.map(keyOf), ...staleKeys];
|
|
1009
|
+
const choiceLabel = (key) => {
|
|
1010
|
+
const [provider, model] = JSON.parse(key);
|
|
1011
|
+
const entry = pool.find((row) => keyOf(row) === key);
|
|
1012
|
+
const candidate = snap.candidates.find((row) => keyOf(row) === key);
|
|
1013
|
+
return { provider, model, name: entry?.name || candidate?.name || model };
|
|
1014
|
+
};
|
|
1015
|
+
const choose = (key, checked, type) => {
|
|
1016
|
+
if (type === 'fixed') {
|
|
1017
|
+
commit((state) => state.strategyDraft ? { ...state, strategyDraft: { ...state.strategyDraft, models: new Set([key]), fixedRoute: key, dirty: true } } : state);
|
|
1018
|
+
} else {
|
|
1019
|
+
commit((state) => {
|
|
1020
|
+
const current = new Set(state.strategyDraft.models);
|
|
1021
|
+
if (checked) current.add(key); else current.delete(key);
|
|
1022
|
+
return { ...state, strategyDraft: { ...state.strategyDraft, models: current, dirty: true } };
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
const typeChoices = h('div', { className: 'lezsp-seg', role: 'group', 'aria-label': t('typeLabel') },
|
|
1027
|
+
h('button', { type: 'button', 'aria-pressed': draft.type === 'fixed', disabled: !editable, onClick: () => switchType('fixed') }, t('typeFixed')),
|
|
1028
|
+
h('button', { type: 'button', 'aria-pressed': draft.type === 'auto', disabled: !editable, onClick: () => switchType('auto') }, t('typeAuto')));
|
|
1029
|
+
return h('section', { 'aria-label': t('editStrategyTitle') },
|
|
1030
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
1031
|
+
button(t('back'), () => { if (discardIfDirty()) closeDetail(); }, { className: 'lezsp-link' }),
|
|
1032
|
+
badge(saved ? (isActive ? t('activeBadge') : t('savedBadge')) : t('unsavedBadge'), isActive ? 'on' : '')),
|
|
1033
|
+
h('h2', { className: 'lezsp-detail-title', tabIndex: -1, id: idFor('strategyDetailTitle'), ref: titleRef('strategyDetail') }, saved ? t('editStrategyTitle') : t('newStrategyTitle')),
|
|
1034
|
+
h('p', { className: 'lezsp-muted' }, isActive ? t('detailSubActive') : saved ? t('detailSubSaved') : t('detailSubNew')),
|
|
1035
|
+
!capacity.known ? hostCapacityNotice() : saveBlocked ? h('p', { className: 'lezsp-note warn', role: 'status' }, saveBlocked) : null,
|
|
1036
|
+
draft.converting ? h('p', { className: 'lezsp-note warn' }, t('convertedHint')) : null,
|
|
1037
|
+
h('form', { onSubmit: (event) => { event.preventDefault(); saveStrategy(); } },
|
|
1038
|
+
h('label', { className: 'lezsp-field', htmlFor: idFor('sName') },
|
|
1039
|
+
h('span', null, t('nameLabel')),
|
|
1040
|
+
h('input', { id: idFor('sName'), type: 'text', maxLength: 60, autoComplete: 'off', value: draft.name, disabled: !editable, placeholder: t('namePlaceholder'), 'aria-invalid': errors.name ? 'true' : undefined, 'aria-describedby': errors.name ? idFor('sName-error') : undefined, onChange: (event) => editStrategyField('name', event.target.value) }),
|
|
1041
|
+
errors.name ? h('small', { className: 'lezsp-error', id: idFor('sName-error') }, errors.name) : null),
|
|
1042
|
+
h('div', { className: 'lezsp-field' }, h('span', null, t('typeLabel')), typeChoices),
|
|
1043
|
+
h('p', { className: 'lezsp-muted' }, draft.type === 'fixed' ? t('typeHintFixed') : t('typeHintAuto')),
|
|
1044
|
+
h('fieldset', { id: idFor('sChoice'), tabIndex: -1, 'aria-label': t(draft.type === 'fixed' ? 'fixedPanel' : 'autoPanel'), 'aria-invalid': (errors.fixed || errors.models) ? 'true' : undefined },
|
|
1045
|
+
h('legend', null, draft.type === 'fixed' ? t('fixedPanel') : t('autoPanel')),
|
|
1046
|
+
h('p', { className: 'lezsp-muted' }, draft.type === 'fixed' ? t('fixedHint') : t('autoHint')),
|
|
1047
|
+
(draft.type === 'fixed' ? fixedChoices : autoChoices).length
|
|
1048
|
+
? h('div', { className: 'lezsp-stack' }, (draft.type === 'fixed' ? fixedChoices : autoChoices).map((key) => {
|
|
1049
|
+
const info = choiceLabel(key); const checked = draft.type === 'fixed' ? draft.fixedRoute === key : draft.models.has(key);
|
|
1050
|
+
return h('label', { key, className: `lezsp-model-choice ${checked ? 'checked' : ''} ${staleKeys.has(key) ? 'stale' : ''}` },
|
|
1051
|
+
h('input', { type: draft.type === 'fixed' ? 'radio' : 'checkbox', name: draft.type === 'fixed' ? idFor('fixedChoice') : undefined, checked, disabled: !editable, 'aria-label': (draft.type === 'fixed' ? t('fixedPanel') : t('autoPanel')) + ': ' + info.name, onChange: (event) => choose(key, event.target.checked, draft.type) }),
|
|
1052
|
+
h('span', { className: 'lezsp-choice-body' },
|
|
1053
|
+
h('span', { className: 'lezsp-choice-name' }, info.name, ' - ', h('span', { className: 'lezsp-muted' }, info.provider), staleKeys.has(key) ? h('span', { className: 'lezsp-error' }, ' ' + t('staleRef')) : null),
|
|
1054
|
+
staleKeys.has(key) ? h('span', { className: 'lezsp-muted' }, t('staleRefHint')) : null));
|
|
1055
|
+
}))
|
|
1056
|
+
: h('p', { className: 'lezsp-muted' }, t(draft.type === 'fixed' ? 'noPoolFixed' : 'noPoolAuto')),
|
|
1057
|
+
errors.fixed ? h('small', { className: 'lezsp-error' }, errors.fixed) : null,
|
|
1058
|
+
errors.models ? h('small', { className: 'lezsp-error' }, errors.models) : null),
|
|
1059
|
+
draft.type === 'auto' ? h('label', { className: 'lezsp-field', htmlFor: idFor('sText') },
|
|
1060
|
+
h('span', null, t('textLabel')),
|
|
1061
|
+
h('textarea', { id: idFor('sText'), rows: 4, maxLength: 2000, value: draft.strategyText, disabled: !editable, 'aria-invalid': errors.text ? 'true' : undefined, onChange: (event) => editStrategyField('strategyText', event.target.value) }),
|
|
1062
|
+
h('small', { className: 'lezsp-muted' }, fmt(t('textCount'), { count: draft.strategyText.length, max: 2000 }) + ' · ' + t('textHint')),
|
|
1063
|
+
errors.text ? h('small', { className: 'lezsp-error' }, errors.text) : null) : null,
|
|
1064
|
+
ui.error?.fields ? h('div', { className: 'lezsp-banner err', role: 'alert' }, t('formError')) : null,
|
|
1065
|
+
h('div', { className: 'lezsp-form-actions' },
|
|
1066
|
+
h('button', { type: 'submit', className: 'lezsp-primary', disabled: !editable }, t('saveStrategy')),
|
|
1067
|
+
button(saved ? t('discard') : t('cancelEdit'), () => {
|
|
1068
|
+
if (!saved) { if (discardIfDirty()) closeDetail(); return; }
|
|
1069
|
+
if (discardIfDirty()) openStrategy(saved.id);
|
|
1070
|
+
}, { disabled: busy }))));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function modelsPage() {
|
|
1074
|
+
if (ui.detail?.kind === 'model' && ui.modelDraft) return modelDetail();
|
|
1075
|
+
const pool = ui.poolDraft;
|
|
1076
|
+
if (!pool) return null;
|
|
1077
|
+
const keys = pool.keys;
|
|
1078
|
+
const savedKeys = new Set(snap.management.pool.map(keyOf));
|
|
1079
|
+
const draftCount = keys.size;
|
|
1080
|
+
const dirty = !sameRouteSet(keys, savedKeys);
|
|
1081
|
+
const removals = [...savedKeys].filter((key) => !keys.has(key));
|
|
1082
|
+
const additions = [...keys].filter((key) => !savedKeys.has(key));
|
|
1083
|
+
const byKey = new Map(snap.management.pool.map((entry) => [keyOf(entry), entry]));
|
|
1084
|
+
const applied = snap.management.active?.snapshot || null;
|
|
1085
|
+
const protect = removals.filter((key) => applied && (applied.models || []).some((route) => keyOf(route) === key));
|
|
1086
|
+
const impact = [];
|
|
1087
|
+
if (removals.length) {
|
|
1088
|
+
const items = removals.map((key) => { const entry = byKey.get(key); return entry?.name || JSON.parse(key)[1]; });
|
|
1089
|
+
impact.push(fmt(t('poolImpactRemove'), { items: items.join('、') }) + (protect.length ? ' ' + t('poolImpactProtect') : ''));
|
|
1090
|
+
}
|
|
1091
|
+
if (additions.length) impact.push(fmt(t('poolImpactAdd'), { items: additions.map((key) => { const candidate = snap.candidates.find((row) => keyOf(row) === key); return candidate?.name || JSON.parse(key)[1]; }).join('、') }));
|
|
1092
|
+
const limits = snap.management.limits;
|
|
1093
|
+
const limitDraft = ui.limitDraft || { maxDepth: '', maxActive: '' };
|
|
1094
|
+
const limitDirty = limitDraft.maxDepth !== (limits ? String(limits.maxDepth) : '') || limitDraft.maxActive !== (limits ? String(limits.maxActive) : '');
|
|
1095
|
+
const depthOver = Boolean(limits && limits.maxDepth > snap.host.maxDepth);
|
|
1096
|
+
const activeOver = Boolean(limits && capacity.known && limits.maxActive > capacity.raw);
|
|
1097
|
+
const conflict = depthOver || activeOver;
|
|
1098
|
+
const effectiveDepth = limits ? Math.min(limits.maxDepth, snap.host.maxDepth) : null;
|
|
1099
|
+
const limitMaxActive = capacity.known ? capacity.ceiling : PLUGIN_MAX_ACTIVE;
|
|
1100
|
+
const limitBlocked = capacityBlockMessage('limits.save');
|
|
1101
|
+
const hostCeilingText = capacity.known
|
|
1102
|
+
? fmt(t('hostCeiling'), { depth: snap.host.maxDepth, active: capacity.raw })
|
|
1103
|
+
: fmt(t('hostCeilingUnknown'), { depth: snap.host.maxDepth, reason: capacity.reason ? fmt(t('hostCeilingReason'), { reason: capacity.reason }) : '' });
|
|
1104
|
+
return h(React.Fragment, null,
|
|
1105
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
1106
|
+
h('div', null, h('h2', { id: idFor('modelsTitle'), tabIndex: -1, ref: titleRef('models') }, t('modelsTitle')), h('p', { className: 'lezsp-sub' }, t('modelsSub'))),
|
|
1107
|
+
badge(fmt(t('modelCount'), { count: snap.candidates.length }))),
|
|
1108
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
1109
|
+
h('div', { className: 'lezsp-row' },
|
|
1110
|
+
button(t('selectAll'), selectAllPool, { className: 'lezsp-small', disabled: !editable }),
|
|
1111
|
+
button(t('clearAll'), clearAllPool, { className: 'lezsp-small', disabled: !editable }),
|
|
1112
|
+
button(t('poolSync'), syncPool, { className: 'lezsp-small', disabled: busy || !snap }),
|
|
1113
|
+
h('span', { className: 'lezsp-count' }, fmt(t('selectedCount'), { count: draftCount }))),
|
|
1114
|
+
h('span', { className: 'lezsp-count' }, fmt(t('savedCount'), { count: savedKeys.size }))),
|
|
1115
|
+
h('p', { className: 'lezsp-muted' }, impact.length ? impact.join(' ') : t('poolImpact')),
|
|
1116
|
+
dirty ? banner(t('poolDirty'), 'warn') : null,
|
|
1117
|
+
h('div', { className: 'lezsp-stack' }, snap.candidates.length ? snap.candidates.map((candidate) => {
|
|
1118
|
+
const key = keyOf(candidate);
|
|
1119
|
+
const checked = keys.has(key);
|
|
1120
|
+
const entry = pool.entries.get(key) || byKey.get(key);
|
|
1121
|
+
const stale = !candidate.authorized || !candidate.available;
|
|
1122
|
+
return h('div', { key, className: `lezsp-pool-row${stale ? ' stale' : ''}` },
|
|
1123
|
+
h('input', { type: 'checkbox', checked, disabled: !editable || (!checked && stale), 'aria-label': fmt(t('poolToggleLabel'), { name: candidate.name || candidate.model }), onChange: (event) => { event.stopPropagation(); togglePool(candidate); } }),
|
|
1124
|
+
h('label', { className: 'lezsp-choice-body' },
|
|
1125
|
+
h('span', { className: 'lezsp-choice-name' }, candidate.name || candidate.model, ' ', h('span', { className: 'lezsp-muted' }, candidate.provider)),
|
|
1126
|
+
h('span', { className: 'lezsp-muted' }, entry?.purpose || candidate.description || t('noPurpose')),
|
|
1127
|
+
h('span', { className: 'lezsp-muted' }, reasoningText(candidate, entry)),
|
|
1128
|
+
stale ? h('span', { className: 'lezsp-error' }, t('unavailableHint')) : null),
|
|
1129
|
+
button(t('abilityCard'), () => openModel(key), { className: 'lezsp-small', disabled: busy || !byKey.has(key), aria: fmt(t('abilityCardLabel'), { name: candidate.name || candidate.model, provider: candidate.provider }) }));
|
|
1130
|
+
}) : h('div', { className: 'lezsp-empty' }, t('emptyModels'))),
|
|
1131
|
+
h('div', { className: 'lezsp-form-actions' },
|
|
1132
|
+
button(t('savePool'), savePool, { className: 'lezsp-primary', disabled: !editable || !dirty }),
|
|
1133
|
+
button(t('discardPool'), () => commit((state) => ({ ...state, poolDraft: draftsFrom(snap).poolDraft, error: null })), { disabled: busy || !dirty })),
|
|
1134
|
+
h('section', { className: 'lezsp-stack', 'aria-labelledby': idFor('limitsTitle') },
|
|
1135
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
1136
|
+
h('h2', { id: idFor('limitsTitle'), tabIndex: -1 }, t('limitsTitle')),
|
|
1137
|
+
badge(t('sharedBadge'))),
|
|
1138
|
+
h('p', { className: 'lezsp-sub' }, t('limitsSub')),
|
|
1139
|
+
hostCapacityNotice(),
|
|
1140
|
+
capacity.known ? h('p', { className: 'lezsp-muted' }, hostCeilingText) : null,
|
|
1141
|
+
h('div', { className: 'lezsp-grid' },
|
|
1142
|
+
h('label', { className: 'lezsp-field', htmlFor: idFor('limDepth') },
|
|
1143
|
+
h('span', null, t('maxDepth')),
|
|
1144
|
+
h('input', { id: idFor('limDepth'), type: 'number', inputMode: 'numeric', min: 0, max: snap.host.maxDepth, step: 1, value: limitDraft.maxDepth, disabled: !editable, placeholder: '0–' + snap.host.maxDepth, 'aria-describedby': idFor('limDepthHint'), onChange: (event) => commit((state) => ({ ...state, limitDraft: { ...state.limitDraft, maxDepth: event.target.value } })) }),
|
|
1145
|
+
h('small', { className: 'lezsp-muted', id: idFor('limDepthHint') }, fmt(t('depthHint'), { max: snap.host.maxDepth }))),
|
|
1146
|
+
h('label', { className: 'lezsp-field', htmlFor: idFor('limActive') },
|
|
1147
|
+
h('span', null, t('maxActive')),
|
|
1148
|
+
h('input', { id: idFor('limActive'), type: 'number', inputMode: 'numeric', min: 1, max: limitMaxActive, step: 1, value: limitDraft.maxActive, disabled: !editable, placeholder: '1–' + limitMaxActive, 'aria-describedby': idFor('limActiveHint'), onChange: (event) => commit((state) => ({ ...state, limitDraft: { ...state.limitDraft, maxActive: event.target.value } })) }),
|
|
1149
|
+
h('small', { className: 'lezsp-muted', id: idFor('limActiveHint') }, capacity.known ? fmt(t('activeHint'), { max: limitMaxActive, host: capacity.raw }) : t('activeHintUnknown')))),
|
|
1150
|
+
h('p', { className: 'lezsp-muted' }, limits ? fmt(t('savedLimits'), { depth: limits.maxDepth, active: limits.maxActive }) : t('noSavedLimits')),
|
|
1151
|
+
h('p', { className: 'lezsp-muted' }, active && limits
|
|
1152
|
+
? (capacity.known
|
|
1153
|
+
? fmt(t('effectiveActive'), { depth: effectiveDepth, active: Math.min(limits.maxActive, capacity.ceiling), local: limits.maxActive, host: capacity.raw })
|
|
1154
|
+
: fmt(t('effectiveUnknown'), { depth: limits.maxDepth, active: limits.maxActive }))
|
|
1155
|
+
: t('effectiveClosed')),
|
|
1156
|
+
limitDirty ? banner(t('limitsDirty'), 'warn') : null,
|
|
1157
|
+
conflict ? banner(fmt(t('hostConflict'), { depth: limits.maxDepth, active: limits.maxActive, ceilingDepth: snap.host.maxDepth, ceilingActive: capacity.known ? capacity.raw : '—' }) + (active ? t('hostConflictActive') : ''), 'warn') : null,
|
|
1158
|
+
h('div', { className: 'lezsp-form-actions' },
|
|
1159
|
+
button(t('saveLimits'), saveLimits, { className: 'lezsp-primary', disabled: !editable || !limitDirty || Boolean(limitBlocked), title: limitBlocked || undefined }),
|
|
1160
|
+
button(t('discardLimits'), () => commit((state) => ({ ...state, limitDraft: draftsFrom(snap).limitDraft, error: null })), { disabled: busy || !limitDirty }))));
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function modelDetail() {
|
|
1164
|
+
const draft = ui.modelDraft;
|
|
1165
|
+
return h('section', { 'aria-label': t('abilityTitle') },
|
|
1166
|
+
button(t('backModels'), () => { if (discardIfDirty()) closeDetail(); }, { className: 'lezsp-link' }),
|
|
1167
|
+
h('h2', { className: 'lezsp-detail-title', tabIndex: -1, id: idFor('modelDetailTitle'), ref: titleRef('modelDetail') },
|
|
1168
|
+
t('abilityTitle'), h('span', { className: 'lezsp-detail-meta' }, `${draft.name || draft.model} · ${draft.provider}`)),
|
|
1169
|
+
h('label', { className: 'lezsp-field', htmlFor: idFor('mPurpose') },
|
|
1170
|
+
h('span', null, t('purposeLabel')),
|
|
1171
|
+
h('input', { id: idFor('mPurpose'), type: 'text', maxLength: 200, value: draft.purpose, disabled: !editable, onChange: (event) => commit((state) => ({ ...state, modelDraft: { ...state.modelDraft, purpose: event.target.value, dirty: true } })) })),
|
|
1172
|
+
h('label', { className: 'lezsp-field', htmlFor: idFor('mNote') },
|
|
1173
|
+
h('span', null, t('noteLabel')),
|
|
1174
|
+
h('textarea', { id: idFor('mNote'), rows: 4, maxLength: 500, value: draft.note, disabled: !editable, onChange: (event) => commit((state) => ({ ...state, modelDraft: { ...state.modelDraft, note: event.target.value, dirty: true } })) })),
|
|
1175
|
+
h('p', { className: 'lezsp-muted' }, t('noteHintUseCase')),
|
|
1176
|
+
h('p', { className: 'lezsp-muted' }, t('noteHintEffect')),
|
|
1177
|
+
h('div', { className: 'lezsp-form-actions' },
|
|
1178
|
+
button(t('saveNote'), saveModelNote, { className: 'lezsp-primary', disabled: !editable || !draft.dirty }),
|
|
1179
|
+
button(t('discardNote'), () => commit((state) => {
|
|
1180
|
+
const current = state.modelDraft;
|
|
1181
|
+
if (!current) return state;
|
|
1182
|
+
const entry = snap.management.pool.find((row) => sameRoute(row, current));
|
|
1183
|
+
return {
|
|
1184
|
+
...state,
|
|
1185
|
+
modelDraft: { ...current, name: entry?.name || current.name, purpose: entry ? entry.purpose || '' : current.purpose, note: entry ? entry.note || '' : current.note, dirty: false },
|
|
1186
|
+
error: null,
|
|
1187
|
+
};
|
|
1188
|
+
}), { disabled: busy || !draft.dirty })));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function recordsPage() {
|
|
1192
|
+
return h(React.Fragment, null,
|
|
1193
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
1194
|
+
h('h2', { id: idFor('recordsTitle'), tabIndex: -1, ref: titleRef('records') }, t('recordsTitle')),
|
|
1195
|
+
badge(String(snap.runs.length))),
|
|
1196
|
+
h('p', { className: 'lezsp-sub' }, t('recordsSub')),
|
|
1197
|
+
h('p', { className: 'lezsp-muted' }, t('recordsHint')),
|
|
1198
|
+
snap.runs.length ? h('div', { className: 'lezsp-stack' }, snap.runs.map((record) => {
|
|
1199
|
+
const name = record.strategyName || (record.strategyId === 'legacy' ? t('legacyStrategyName') : '') || t('legacyStrategyName');
|
|
1200
|
+
const status = record.status === 'started' ? t('statusStarted') : record.status === 'disposed' ? t('statusDisposed') : t('statusAdmitted');
|
|
1201
|
+
const actual = record.evidence === 'assistant/message' && record.actualModel ? routeLabel({ provider: record.actualProvider, model: record.actualModel }) : t('noActual');
|
|
1202
|
+
const evidence = record.evidence === 'assistant/message' ? 'assistant/message' : record.evidence === 'request/header (尚无响应)' ? t('evidenceRequested') : record.evidence === 'pending' ? t('evidencePending') : (record.evidence || t('noActual'));
|
|
1203
|
+
return h('article', { key: record.id, className: 'lezsp-record' },
|
|
1204
|
+
h('details', null,
|
|
1205
|
+
h('summary', null, h('span', { className: 'lezsp-record-head' },
|
|
1206
|
+
h('span', { className: 'lezsp-time' }, formatTime(record.createdAt)),
|
|
1207
|
+
h('strong', null, name),
|
|
1208
|
+
h('span', { className: 'lezsp-muted' }, record.label || record.id),
|
|
1209
|
+
h('span', { className: 'lezsp-status-tag' }, status))),
|
|
1210
|
+
h('div', { className: 'lezsp-record-body' },
|
|
1211
|
+
h('dl', null,
|
|
1212
|
+
h('dt', null, t('requested')), h('dd', { className: 'lezsp-route' }, routeLabel({ provider: record.requestedProvider, model: record.requestedModel }) || t('noRequest')),
|
|
1213
|
+
h('dt', null, t('actual')), h('dd', { className: 'lezsp-route' }, actual),
|
|
1214
|
+
h('dt', null, t('evidence')), h('dd', null, evidence),
|
|
1215
|
+
h('dt', null, t('status')), h('dd', null, status),
|
|
1216
|
+
h('dt', null, t('recordedAt')), h('dd', null, formatTime(record.createdAt))))));
|
|
1217
|
+
})) : h('div', { className: 'lezsp-empty' }, t('noRecords')));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (!sessionId) return h('div', { className: 'lezsp' }, h('style', null, CSS), t('noSession'));
|
|
1221
|
+
return h('div', { className: 'lezsp', 'aria-label': t('title'), 'aria-busy': busy },
|
|
1222
|
+
h('style', null, CSS),
|
|
1223
|
+
h('header', null,
|
|
1224
|
+
h('div', { className: 'lezsp-between lezsp-row' },
|
|
1225
|
+
h('div', null, h('h1', null, t('title')), h('p', { className: 'lezsp-sub' }, t('description'))),
|
|
1226
|
+
h('div', { className: 'lezsp-row' },
|
|
1227
|
+
h('span', { className: 'lezsp-session-chip', role: busy ? 'status' : undefined, 'aria-live': busy ? 'polite' : undefined }, busy ? t(ui.busy === 'read' ? 'loading' : 'saving') : t('scope')),
|
|
1228
|
+
busy ? button(t('cancel'), cancel, { className: 'lezsp-small' }) : null))),
|
|
1229
|
+
h('nav', { className: 'lezsp-nav', 'aria-label': t('navLabel') },
|
|
1230
|
+
['strategies', 'models', 'records'].map((key) => h('button', { key, type: 'button', 'aria-current': page === key ? 'page' : undefined, onClick: () => goPage(key) }, t(key === 'strategies' ? 'pageStrategies' : key === 'models' ? 'pageModels' : 'pageRecords')))),
|
|
1231
|
+
ui.error ? h('div', { className: 'lezsp-banner err', role: 'alert' },
|
|
1232
|
+
h('p', null, `${t('error')}: ${ui.error.message}`),
|
|
1233
|
+
snap ? h('div', { className: 'lezsp-actions' }, button(t('retry'), read, { disabled: busy })) : null) : null,
|
|
1234
|
+
ui.notice ? banner(ui.notice, ui.noticeKind) : null,
|
|
1235
|
+
ui.mustRefresh ? h('div', { className: 'lezsp-banner warn' }, h('p', null, t('mustRefresh')), h('div', { className: 'lezsp-actions' }, button(t('refresh'), read, { disabled: busy }))) : null,
|
|
1236
|
+
ui.conflict ? h('div', { className: 'lezsp-banner err', role: 'alert' },
|
|
1237
|
+
h('strong', null, t('conflict')), h('p', null, t('conflictHelp')),
|
|
1238
|
+
h('div', { className: 'lezsp-actions' }, button(t('loadServer'), loadServer, { disabled: busy }))) : null,
|
|
1239
|
+
!snap ? h('div', { className: 'lezsp-banner' },
|
|
1240
|
+
h('p', null, t('noSnapshot')),
|
|
1241
|
+
button(t('retry'), read, { disabled: busy })) : h(React.Fragment, null,
|
|
1242
|
+
!snap.canEdit ? banner(t('readOnly')) : null,
|
|
1243
|
+
page === 'strategies' ? strategiesPage() : page === 'models' ? modelsPage() : recordsPage()));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function PolicyTab(props) {
|
|
1247
|
+
const { tab } = props.useTabInfo();
|
|
1248
|
+
return h(PolicySession, { key: props.sessionId, sessionId: props.sessionId, t: props.t, visible: tab.visible, tabSignal: tab.signal });
|
|
1249
|
+
}
|
|
1250
|
+
function HeaderButton({ sessionId, t }) {
|
|
1251
|
+
const [error, setError] = useState('');
|
|
1252
|
+
return h('span', { className: 'lezsp-header' }, h('style', null, CSS),
|
|
1253
|
+
h('button', { type: 'button', title: t('open'), 'aria-label': t('open'), disabled: !sessionId, onClick: () => {
|
|
1254
|
+
try { ctx.sidebarRight.openTab(KIND); setError(''); } catch (failure) { setError(failure?.message || String(failure)); }
|
|
1255
|
+
} }, h(PolicyIcon, { size: 18 })),
|
|
1256
|
+
error ? h('span', { role: 'alert', title: error }, error) : null);
|
|
1257
|
+
}
|
|
1258
|
+
ctx.effect(() => ctx.sidebarRightTabs.register({
|
|
1259
|
+
id: ID, kind: KIND, title: () => translate('title'), keepMounted: true,
|
|
1260
|
+
guide: [{ id: 'session-policy', order: 45, title: () => translate('title'), description: () => translate('description'), icon: PolicyIcon }],
|
|
1261
|
+
}), `${ID}: tab type and native guide card`);
|
|
1262
|
+
ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
|
|
1263
|
+
name: 'sidebar.right.pane.tab', key: ID, locale: NS,
|
|
1264
|
+
}, PolicyTab)), `${ID}: policy tab body`);
|
|
1265
|
+
ctx.effect(() => ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
1266
|
+
name: 'conversation.session.header.utilities', id: ID, order: 45, locale: NS,
|
|
1267
|
+
}, HeaderButton)), `${ID}: session header opener`);
|
|
1268
|
+
},
|
|
1269
|
+
};
|
|
1270
|
+
},
|
|
1271
|
+
});
|