@mokuyoaxis/dsh-iris 0.1.1 → 0.1.2
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 +42 -1
- package/README.en.md +245 -0
- package/README.md +55 -6
- package/cordis.patch.yml +1 -1
- package/docs/ROADMAP.md +22 -5
- package/docs/assets/diagrams/iris-architecture.drawio +1 -0
- package/docs/assets/diagrams/iris-architecture.png +0 -0
- package/docs/assets/diagrams/iris-architecture.svg +63 -0
- package/docs/assets/diagrams/iris-task-lifecycle.drawio +1 -0
- package/docs/assets/diagrams/iris-task-lifecycle.png +0 -0
- package/docs/assets/diagrams/iris-task-lifecycle.svg +64 -0
- package/docs/assets/diagrams/iris-workflow-compose.drawio +1 -0
- package/docs/assets/diagrams/iris-workflow-compose.png +0 -0
- package/docs/assets/diagrams/iris-workflow-compose.svg +41 -0
- package/docs/assets/examples/iris-flower-generated.webp +0 -0
- package/docs/assets/screenshots/image-task-succeeded.webp +0 -0
- package/docs/assets/screenshots/prompt-general-after.webp +0 -0
- package/docs/assets/screenshots/prompt-general-before.webp +0 -0
- package/docs/assets/screenshots/prompt-general-preview.webp +0 -0
- package/docs/assets/screenshots/prompt-image-before.webp +0 -0
- package/docs/assets/screenshots/prompt-image-preview.webp +0 -0
- package/docs/releases/0.1.2.md +26 -0
- package/docs/screenshots.md +34 -0
- package/lib/api.js +42 -11
- package/lib/bundled-skills.js +62 -0
- package/lib/client.js +352 -9
- package/lib/index.js +11 -1
- package/lib/prompt-optimizer-config.js +170 -0
- package/lib/prompt-optimizer.js +244 -0
- package/package.json +7 -3
- package/screenshots.json +6 -0
- package/user_guide.md +64 -3
package/lib/client.js
CHANGED
|
@@ -111,7 +111,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
111
111
|
'.iris-bubble.lit .iris-bubble-btn { border-color: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 14px var(--dsw-alias-state-success-primary, #6fcf6f); }',
|
|
112
112
|
'.iris-bubble.dim .iris-bubble-btn { opacity: 0.55; border-color: var(--dsw-alias-border-l1, #3a3a44); }',
|
|
113
113
|
'.iris-bubble-badge { position: absolute; top: -2px; right: -2px; min-width: 16px; height: 16px; border-radius: 8px; background: var(--dsw-alias-state-warn-primary, #d9a941); color: #1a1a1e; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; padding: 0 3px; }',
|
|
114
|
-
'.iris-bubble-panel { position: fixed; z-index: 9998; width: 360px; max-height: min(520px, 70vh); overflow: auto; background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 12px; padding: 12px 14px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); }',
|
|
114
|
+
'.iris-bubble-panel { position: fixed; z-index: 9998; width: 360px; max-height: min(520px, 70vh); overflow: auto; background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 12px; padding: 12px 14px; font-size: 12px; line-height: 1.45; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); }',
|
|
115
115
|
'.iris-bubble-panel-head { position: absolute; top: 6px; right: 6px; z-index: 2; }',
|
|
116
116
|
'.iris-bubble-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 14px; cursor: pointer; padding: 4px 6px; }',
|
|
117
117
|
'.iris-bubble-close:hover { color: var(--dsw-alias-label-primary, #e6e6e6); }',
|
|
@@ -160,7 +160,48 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
160
160
|
'.iris-ff-list { display: grid; gap: 2px; max-height: 160px; overflow: auto; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 6px; padding: 4px; background: var(--dsw-alias-bg-layer-2, #32323c); }',
|
|
161
161
|
'.iris-ff-item { display: flex; align-items: center; gap: 6px; font-size: 11px; cursor: pointer; padding: 2px 4px; border-radius: 4px; }',
|
|
162
162
|
'.iris-ff-item:hover { background: var(--dsw-alias-bg-layer-1, #26262e); }',
|
|
163
|
-
'.iris-ff-item .iris-ff-src { color: var(--dsw-alias-brand-primary, #7aa2f7); font-size: 10px; }'
|
|
163
|
+
'.iris-ff-item .iris-ff-src { color: var(--dsw-alias-brand-primary, #7aa2f7); font-size: 10px; }',
|
|
164
|
+
/* 对话框级提示词优化器:桌面浮层 + 窄屏底部 Sheet */
|
|
165
|
+
'.iris-po-wrap { position: relative; display: inline-flex; flex: 0 0 auto; }',
|
|
166
|
+
'.iris-po-trigger { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: 0; outline: 0; background: transparent; color: inherit; font-size: 19px; line-height: 1; cursor: pointer; box-shadow: none; filter: drop-shadow(0 3px 7px rgba(91,113,220,.22)); transition: transform .18s ease, filter .18s ease, opacity .18s ease; }',
|
|
167
|
+
'.iris-po-trigger:hover { transform: translateY(-2px) scale(1.10); filter: drop-shadow(0 5px 10px rgba(112,137,255,.42)); }',
|
|
168
|
+
'.iris-po-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #7aa2f7); outline-offset: 2px; }',
|
|
169
|
+
'.iris-po-backdrop { position: fixed; inset: 0; z-index: 10019; background: rgba(8,10,18,.30); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); touch-action: none; animation: iris-po-fade .16s ease-out; }',
|
|
170
|
+
'.iris-po-panel { position: fixed; z-index: 10020; right: max(14px, env(safe-area-inset-right)); bottom: max(78px, calc(env(safe-area-inset-bottom) + 68px)); width: min(440px, calc(100vw - 28px)); max-height: min(680px, calc(100dvh - 104px)); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; box-sizing: border-box; padding: 18px; display: grid; gap: 13px; background-color: rgba(35,36,47,.84); background: radial-gradient(circle at 10% -5%, rgba(132,160,255,.24), transparent 42%), color-mix(in srgb, var(--dsw-alias-bg-layer-1, #26262e) 82%, transparent); color: var(--dsw-alias-label-primary, #eeeeF2); border: 1px solid rgba(150,164,215,.25); border-radius: 20px; box-shadow: 0 24px 70px rgba(0,0,0,.48), inset 0 1px 0 rgba(255,255,255,.07); backdrop-filter: blur(28px) saturate(1.28); -webkit-backdrop-filter: blur(28px) saturate(1.28); animation: iris-po-rise .20s cubic-bezier(.2,.8,.2,1); scrollbar-width: thin; }',
|
|
171
|
+
'.iris-po-head { position: sticky; top: -18px; z-index: 1; margin: -18px -18px 0; padding: 16px 18px 10px; display: flex; align-items: center; justify-content: space-between; gap: 10px; background: linear-gradient(to bottom, color-mix(in srgb, var(--dsw-alias-bg-layer-1, #26262e) 88%, transparent) 72%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }',
|
|
172
|
+
'.iris-po-title { display: inline-flex; align-items: center; gap: 9px; font-size: 14px; letter-spacing: .01em; }',
|
|
173
|
+
'.iris-po-mark, .iris-po-settings-mark { display: inline-flex; align-items: center; justify-content: center; width: 29px; height: 29px; border-radius: 999px; background: rgba(255,255,255,.055); box-shadow: inset 0 1px 0 rgba(255,255,255,.10), 0 5px 16px rgba(67,83,168,.16); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); filter: drop-shadow(0 2px 5px rgba(105,128,235,.16)); }',
|
|
174
|
+
'.iris-po-close { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 999px; background: rgba(255,255,255,.05); color: var(--dsw-alias-label-secondary, #a6a6b2); font-size: 14px; cursor: pointer; transition: background .15s, color .15s; }',
|
|
175
|
+
'.iris-po-close:hover { background: rgba(255,255,255,.10); color: var(--dsw-alias-label-primary, #eeeef2); }',
|
|
176
|
+
'.iris-po-muted, .iris-po-route, .iris-po-note { font-size: 11px; line-height: 1.55; color: var(--dsw-alias-label-secondary, #a6a6b2); word-break: break-word; }',
|
|
177
|
+
'.iris-po-route { padding: 7px 10px; border-radius: 10px; background: rgba(255,255,255,.045); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
|
|
178
|
+
'.iris-po-note { padding: 8px 10px; border-radius: 10px; color: var(--dsw-alias-state-warn-primary, #dfbd69); background: rgba(217,169,65,.08); }',
|
|
179
|
+
'.iris-po-field { display: grid; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
|
|
180
|
+
'.iris-po-field select, .iris-po-result textarea { width: 100%; box-sizing: border-box; background: rgba(255,255,255,.055); color: var(--dsw-alias-label-primary, #eeeef2); border: 1px solid rgba(150,164,215,.20); border-radius: 11px; padding: 9px 11px; font: inherit; font-size: 12px; outline: none; transition: border-color .15s, background .15s, box-shadow .15s; }',
|
|
181
|
+
'.iris-po-field select:focus, .iris-po-result textarea:focus { border-color: rgba(122,162,247,.72); background: rgba(255,255,255,.075); box-shadow: 0 0 0 3px rgba(122,162,247,.11); }',
|
|
182
|
+
'.iris-po-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
|
|
183
|
+
'.iris-po-btn, .iris-po-primary { min-height: 34px; border: 1px solid rgba(150,164,215,.20); border-radius: 10px; padding: 6px 11px; font: inherit; font-size: 11px; cursor: pointer; transition: transform .15s, background .15s, border-color .15s; }',
|
|
184
|
+
'.iris-po-btn { background: rgba(255,255,255,.045); color: var(--dsw-alias-label-secondary, #b0b0bb); }',
|
|
185
|
+
'.iris-po-btn:hover { background: rgba(255,255,255,.085); color: var(--dsw-alias-label-primary, #eeeef2); border-color: rgba(150,174,255,.50); }',
|
|
186
|
+
'.iris-po-primary { background: linear-gradient(135deg, #779cf3, #8c79de); color: #fff; border-color: rgba(174,187,255,.55); font-weight: 650; box-shadow: 0 6px 18px rgba(76,92,188,.22); }',
|
|
187
|
+
'.iris-po-primary:hover { transform: translateY(-1px); box-shadow: 0 8px 22px rgba(76,92,188,.32); }',
|
|
188
|
+
'.iris-po-primary:disabled, .iris-po-btn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: none; }',
|
|
189
|
+
'.iris-po-result { display: grid; gap: 8px; padding: 12px; border: 1px solid rgba(137,155,218,.18); border-radius: 15px; background: rgba(10,12,20,.16); }',
|
|
190
|
+
'.iris-po-result-label { font-size: 11px; font-weight: 650; letter-spacing: .02em; }',
|
|
191
|
+
'.iris-po-result textarea { min-height: 156px; max-height: 34dvh; resize: vertical; line-height: 1.58; }',
|
|
192
|
+
'.iris-po-config { padding: 11px 12px; border: 1px solid rgba(150,164,215,.14); border-radius: 13px; background: rgba(255,255,255,.025); font-size: 11px; }',
|
|
193
|
+
'.iris-po-config summary { cursor: pointer; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
|
|
194
|
+
'.iris-po-config[open] summary { margin-bottom: 9px; color: var(--dsw-alias-label-primary, #eeeef2); }',
|
|
195
|
+
'.iris-po-config .iris-po-actions { margin: 9px 0; }',
|
|
196
|
+
'.iris-po-btn.danger:hover { color: var(--dsw-alias-state-error-primary, #eb7d86); border-color: rgba(235,125,134,.55); background: rgba(235,125,134,.08); }',
|
|
197
|
+
'.iris-po-file { display: none; }',
|
|
198
|
+
'.iris-po-disable { justify-self: start; border: 0; padding: 2px 0; background: transparent; color: var(--dsw-alias-label-tertiary, #777986); font: inherit; font-size: 10px; cursor: pointer; }',
|
|
199
|
+
'.iris-po-disable:hover { color: var(--dsw-alias-state-error-primary, #eb7d86); }',
|
|
200
|
+
'@keyframes iris-po-fade { from { opacity: 0; } to { opacity: 1; } }',
|
|
201
|
+
'@keyframes iris-po-rise { from { opacity: 0; transform: translateY(12px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } }',
|
|
202
|
+
'@media (max-width: 640px) { .iris-po-backdrop { background: rgba(6,8,14,.48); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); } .iris-po-panel { left: max(8px, env(safe-area-inset-left)); right: max(8px, env(safe-area-inset-right)); bottom: max(8px, env(safe-area-inset-bottom)); width: auto; max-height: calc(100dvh - max(16px, env(safe-area-inset-top))); padding: 16px; gap: 11px; border-radius: 22px; } .iris-po-head { top: -16px; margin: -16px -16px 0; padding: 14px 16px 10px; } .iris-po-btn, .iris-po-primary { min-height: 42px; flex: 1 1 auto; } .iris-po-actions .iris-po-primary:first-child { flex-basis: 100%; } .iris-po-result textarea { min-height: 122px; max-height: 28dvh; resize: none; } .iris-po-route { white-space: normal; } }',
|
|
203
|
+
'@media (max-width: 360px) { .iris-po-panel { left: 4px; right: 4px; bottom: max(4px, env(safe-area-inset-bottom)); padding: 13px; border-radius: 18px; } .iris-po-head { top: -13px; margin: -13px -13px 0; padding: 12px 13px 8px; } .iris-po-actions { gap: 6px; } }',
|
|
204
|
+
'@media (prefers-reduced-motion: reduce) { .iris-po-panel, .iris-po-backdrop { animation: none; } .iris-po-trigger, .iris-po-btn, .iris-po-primary { transition: none; } }',
|
|
164
205
|
].join('\n');
|
|
165
206
|
|
|
166
207
|
function adoptStyles() {
|
|
@@ -250,6 +291,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
250
291
|
return sharedState;
|
|
251
292
|
}
|
|
252
293
|
|
|
294
|
+
/** 功能卡就绪态直接来自共享 Iris 状态,不再等用户点开卡片后才加载。 */
|
|
295
|
+
function capabilityReady(state, capability) {
|
|
296
|
+
if (!capability) return true;
|
|
297
|
+
var providers = (state && state.providers) || [];
|
|
298
|
+
return providers.some(function (p) {
|
|
299
|
+
return p.enabled && p.apiKeyHint && Array.isArray(p.capabilities) && p.capabilities.indexOf(capability) >= 0;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
253
303
|
/* ---- 任务详情抽屉:按需拉取 /iris/api/task/:id ---- */
|
|
254
304
|
function useTaskDetail(taskId) {
|
|
255
305
|
var pair = React.useState(null);
|
|
@@ -650,6 +700,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
650
700
|
var modelPair = React.useState(null); // { assigned, options:[{id,providerId}], tested }
|
|
651
701
|
var modelInfo = modelPair[0];
|
|
652
702
|
var setModelInfo = modelPair[1];
|
|
703
|
+
var irisState = useIrisState(5000);
|
|
653
704
|
function setVal(key, value) { var nv = {}; Object.keys(vals).forEach(function (k) { nv[k] = vals[k]; }); nv[key] = value; setVals(nv); }
|
|
654
705
|
function loadModels() {
|
|
655
706
|
if (!capability) return;
|
|
@@ -729,11 +780,13 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
729
780
|
run === 'running' ? '执行中…' : '执行'),
|
|
730
781
|
result);
|
|
731
782
|
}
|
|
732
|
-
var
|
|
783
|
+
var headError = !!(run && run !== 'running' && run.ok === false);
|
|
784
|
+
var headLit = !capability || (capabilityReady(irisState, capability) && !headError);
|
|
785
|
+
var headTitle = headError ? '最近一次执行出错' : (headLit ? 'API 与能力已配置' : '缺少可用 API 或能力模型');
|
|
733
786
|
return React.createElement('div', { className: 'iris-act-card' + (open ? ' open' : '') + (headLit ? ' lit' : ' dim') },
|
|
734
787
|
React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
|
|
735
788
|
React.createElement('span', { className: 'iris-act-title' }, title),
|
|
736
|
-
capability ? React.createElement('span', { className: headLit ? 'iris-pm-dot lit' : 'iris-pm-dot', title:
|
|
789
|
+
capability ? React.createElement('span', { className: headLit ? 'iris-pm-dot lit' : 'iris-pm-dot', title: headTitle }, '●') : null,
|
|
737
790
|
React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
|
|
738
791
|
body);
|
|
739
792
|
}
|
|
@@ -957,6 +1010,53 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
957
1010
|
note ? React.createElement('div', { className: 'iris-clean-note', style: { width: '100%' } }, note) : null);
|
|
958
1011
|
}
|
|
959
1012
|
|
|
1013
|
+
function PromptOptimizerSettings() {
|
|
1014
|
+
var pair = React.useState(null);
|
|
1015
|
+
var state = pair[0];
|
|
1016
|
+
var setState = pair[1];
|
|
1017
|
+
var notePair = React.useState('');
|
|
1018
|
+
var note = notePair[0];
|
|
1019
|
+
var setNote = notePair[1];
|
|
1020
|
+
|
|
1021
|
+
function load() {
|
|
1022
|
+
fetch('/iris/api/prompt-optimizer/config')
|
|
1023
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
1024
|
+
.then(function (data) { if (data && data.config) setState(data); })
|
|
1025
|
+
.catch(function () { setNote('读取提示词优化配置失败'); });
|
|
1026
|
+
}
|
|
1027
|
+
React.useEffect(function () {
|
|
1028
|
+
function changed(event) { if (event.detail && event.detail.config) setState(event.detail); }
|
|
1029
|
+
window.addEventListener('iris-prompt-config-changed', changed);
|
|
1030
|
+
load();
|
|
1031
|
+
return function () { window.removeEventListener('iris-prompt-config-changed', changed); };
|
|
1032
|
+
}, []);
|
|
1033
|
+
|
|
1034
|
+
function toggle() {
|
|
1035
|
+
var enabled = !(state && state.config && state.config.enabled !== false);
|
|
1036
|
+
fetch('/iris/api/prompt-optimizer/enabled', {
|
|
1037
|
+
method: 'POST',
|
|
1038
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1039
|
+
body: JSON.stringify({ enabled: enabled })
|
|
1040
|
+
}).then(function (res) {
|
|
1041
|
+
return res.json().then(function (data) {
|
|
1042
|
+
if (!res.ok) throw new Error(data.error || '更新失败');
|
|
1043
|
+
setState(data);
|
|
1044
|
+
window.dispatchEvent(new CustomEvent('iris-prompt-config-changed', { detail: data }));
|
|
1045
|
+
setNote(enabled ? '对话框 🫧 入口已启用' : '对话框入口已关闭;Iris 工作台与后台能力不受影响');
|
|
1046
|
+
});
|
|
1047
|
+
}).catch(function (err) { setNote(String(err && err.message || err)); });
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
var enabled = !state || !state.config || state.config.enabled !== false;
|
|
1051
|
+
return React.createElement('div', { className: 'iris-wb-card' },
|
|
1052
|
+
React.createElement('div', { className: 'iris-wb-row' },
|
|
1053
|
+
React.createElement('span', { className: 'iris-po-settings-mark' }, '🫧'),
|
|
1054
|
+
React.createElement('span', { style: { flex: 1 } }, enabled ? '对话框入口已启用' : '对话框入口已关闭'),
|
|
1055
|
+
React.createElement('button', { className: 'iris-pm-btn', onClick: toggle }, enabled ? '关闭入口' : '重新启用')),
|
|
1056
|
+
React.createElement('div', { className: 'iris-wb-muted' }, '这里只控制对话框提示词优化入口;Iris 工作台、Agent 工具和任务后台始终保持运行。'),
|
|
1057
|
+
note ? React.createElement('div', { className: 'iris-clean-note' }, note) : null);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
960
1060
|
function WorkbenchPanel() {
|
|
961
1061
|
var state = useIrisState(5000);
|
|
962
1062
|
var running = (state && state.tasks && state.tasks.running) || [];
|
|
@@ -975,6 +1075,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
975
1075
|
React.createElement(CapabilityAssigner, {}),
|
|
976
1076
|
React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
|
|
977
1077
|
React.createElement(BubbleCardPicker, {}),
|
|
1078
|
+
React.createElement('div', { className: 'iris-wb-sec' }, '对话框提示词优化'),
|
|
1079
|
+
React.createElement(PromptOptimizerSettings, {}),
|
|
978
1080
|
React.createElement('div', { className: 'iris-wb-sec' }, '运行中任务'),
|
|
979
1081
|
React.createElement('div', { className: 'iris-wb-box' },
|
|
980
1082
|
running.length ? running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })
|
|
@@ -1002,17 +1104,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1002
1104
|
function onSelect(id) { setSelectedTask(id); }
|
|
1003
1105
|
var body;
|
|
1004
1106
|
if (tab === 'tasks') {
|
|
1005
|
-
|
|
1107
|
+
// 快捷面板只承载当前任务与可直接查看的成功产物;失败/取消历史留在完整工作台。
|
|
1108
|
+
var succeeded = recent.filter(function (t) {
|
|
1109
|
+
return t.status === 'succeeded' && Array.isArray(t.media) && t.media.length > 0;
|
|
1110
|
+
});
|
|
1111
|
+
var shown = succeeded.slice(0, 6);
|
|
1006
1112
|
body = React.createElement('div', { className: 'iris-wb-box' },
|
|
1007
1113
|
running.length ? React.createElement('div', {},
|
|
1008
1114
|
React.createElement('div', { className: 'iris-wb-sec' }, '运行中'),
|
|
1009
1115
|
running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })) : null,
|
|
1010
|
-
React.createElement('div', { className: 'iris-wb-sec' }, '
|
|
1116
|
+
React.createElement('div', { className: 'iris-wb-sec' }, '最近完成'),
|
|
1011
1117
|
shown.length ? shown.map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })
|
|
1012
|
-
: React.createElement('div', { className: 'iris-wb-empty' }, '
|
|
1013
|
-
|
|
1118
|
+
: React.createElement('div', { className: 'iris-wb-empty' }, '尚无成功产物'),
|
|
1119
|
+
succeeded.length > 6 || recentTotal > succeeded.length
|
|
1014
1120
|
? React.createElement('div', { className: 'iris-bubble-more', style: { cursor: 'default' } },
|
|
1015
|
-
'
|
|
1121
|
+
'失败、取消与完整历史请到 Iris 工作台查看') : null);
|
|
1016
1122
|
} else {
|
|
1017
1123
|
var defs = CARD_DEFS.filter(function (d) { return cards.indexOf(d.action) >= 0; });
|
|
1018
1124
|
body = defs.length
|
|
@@ -1027,6 +1133,234 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1027
1133
|
selectedTask ? React.createElement(TaskDetailDrawer, { taskId: selectedTask }) : null);
|
|
1028
1134
|
}
|
|
1029
1135
|
|
|
1136
|
+
function PromptOptimizerControl(props) {
|
|
1137
|
+
var draft = props.useInput(function (value) { return value.draft; });
|
|
1138
|
+
var phase = props.useInput(function (value) { return value.phase; });
|
|
1139
|
+
var refCount = props.useInput(function (value) { return (value.occurrences || []).length; });
|
|
1140
|
+
var projection = props.useProjection('modelSelection');
|
|
1141
|
+
var selectedRoute = projection && (projection.current || projection.selection || projection.next || projection.lastUsed || (projection.provider && projection.model ? projection : null));
|
|
1142
|
+
|
|
1143
|
+
var openPair = React.useState(false);
|
|
1144
|
+
var open = openPair[0];
|
|
1145
|
+
var setOpen = openPair[1];
|
|
1146
|
+
var targetPair = React.useState('general');
|
|
1147
|
+
var target = targetPair[0];
|
|
1148
|
+
var setTarget = targetPair[1];
|
|
1149
|
+
var busyPair = React.useState(false);
|
|
1150
|
+
var busy = busyPair[0];
|
|
1151
|
+
var setBusy = busyPair[1];
|
|
1152
|
+
var resultPair = React.useState(null);
|
|
1153
|
+
var result = resultPair[0];
|
|
1154
|
+
var setResult = resultPair[1];
|
|
1155
|
+
var notePair = React.useState('');
|
|
1156
|
+
var note = notePair[0];
|
|
1157
|
+
var setNote = notePair[1];
|
|
1158
|
+
var configPair = React.useState(null);
|
|
1159
|
+
var configState = configPair[0];
|
|
1160
|
+
var setConfigState = configPair[1];
|
|
1161
|
+
var abortRef = React.useRef(null);
|
|
1162
|
+
var fileRef = React.useRef(null);
|
|
1163
|
+
|
|
1164
|
+
function promptRequest(operation, body, signal) {
|
|
1165
|
+
return fetch('/iris/api/prompt-optimizer/' + operation, {
|
|
1166
|
+
method: 'POST',
|
|
1167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1168
|
+
body: JSON.stringify(body || {}),
|
|
1169
|
+
signal: signal
|
|
1170
|
+
}).then(function (res) {
|
|
1171
|
+
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
1172
|
+
if (!res.ok) throw new Error(data.error || ('请求失败 (' + res.status + ')'));
|
|
1173
|
+
return data;
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function loadConfig() {
|
|
1179
|
+
fetch('/iris/api/prompt-optimizer/config')
|
|
1180
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
1181
|
+
.then(function (data) { if (data && data.config) setConfigState(data); })
|
|
1182
|
+
.catch(function () { setNote('无法读取提示词优化配置'); });
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
React.useEffect(function () {
|
|
1186
|
+
return function () {
|
|
1187
|
+
if (abortRef.current) abortRef.current.abort();
|
|
1188
|
+
};
|
|
1189
|
+
}, []);
|
|
1190
|
+
|
|
1191
|
+
React.useEffect(function () {
|
|
1192
|
+
function changed(event) { if (event.detail && event.detail.config) setConfigState(event.detail); }
|
|
1193
|
+
window.addEventListener('iris-prompt-config-changed', changed);
|
|
1194
|
+
loadConfig();
|
|
1195
|
+
return function () { window.removeEventListener('iris-prompt-config-changed', changed); };
|
|
1196
|
+
}, []);
|
|
1197
|
+
|
|
1198
|
+
function publishConfig(data) {
|
|
1199
|
+
setConfigState(data);
|
|
1200
|
+
try { window.dispatchEvent(new CustomEvent('iris-prompt-config-changed', { detail: data })); } catch (_) {}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function optimize() {
|
|
1204
|
+
if (!draft || !draft.trim()) { setNote('请先在输入框中写入提示词'); return; }
|
|
1205
|
+
if (refCount) { setNote('当前版本暂不改写含 @ 或 / 结构化引用的草稿,以免引用失效'); return; }
|
|
1206
|
+
if (phase !== 'plain') { setNote('输入框正忙,请稍后再试'); return; }
|
|
1207
|
+
if (abortRef.current) abortRef.current.abort();
|
|
1208
|
+
var controller = new AbortController();
|
|
1209
|
+
abortRef.current = controller;
|
|
1210
|
+
setBusy(true);
|
|
1211
|
+
setNote('正在调用模型优化…这可能产生模型费用');
|
|
1212
|
+
setResult(null);
|
|
1213
|
+
promptRequest('optimize', {
|
|
1214
|
+
text: draft,
|
|
1215
|
+
target: target,
|
|
1216
|
+
sessionId: props.sessionId,
|
|
1217
|
+
route: selectedRoute || undefined
|
|
1218
|
+
}, controller.signal).then(function (data) {
|
|
1219
|
+
setResult(data);
|
|
1220
|
+
setNote('已生成预览,尚未写回输入框');
|
|
1221
|
+
}).catch(function (err) {
|
|
1222
|
+
if (controller.signal.aborted) setNote('已取消');
|
|
1223
|
+
else setNote(String(err && err.message || err));
|
|
1224
|
+
}).finally(function () {
|
|
1225
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
1226
|
+
setBusy(false);
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function cancel() {
|
|
1231
|
+
if (abortRef.current) abortRef.current.abort();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function replaceDraft(text) {
|
|
1235
|
+
if (!result) return;
|
|
1236
|
+
if (draft !== result.original && !window.confirm('输入框内容在优化期间已变化,仍要替换吗?')) return;
|
|
1237
|
+
props.inputActions.setDraft(text);
|
|
1238
|
+
setNote(text === result.original ? '已恢复优化前原文' : '已写回输入框;不会自动发送');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function copyResult() {
|
|
1242
|
+
if (!result) return;
|
|
1243
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
1244
|
+
navigator.clipboard.writeText(result.optimized).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
|
|
1245
|
+
} else {
|
|
1246
|
+
setNote('浏览器不支持自动复制,请手动选择文本');
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function importConfig(event) {
|
|
1251
|
+
var file = event.target.files && event.target.files[0];
|
|
1252
|
+
event.target.value = '';
|
|
1253
|
+
if (!file) return;
|
|
1254
|
+
file.text().then(function (raw) {
|
|
1255
|
+
var parsed = JSON.parse(raw);
|
|
1256
|
+
return promptRequest('import', { config: parsed });
|
|
1257
|
+
}).then(function (data) {
|
|
1258
|
+
publishConfig(data);
|
|
1259
|
+
setNote('JSON 配置已导入并生效');
|
|
1260
|
+
}).catch(function (err) {
|
|
1261
|
+
setNote('导入失败:' + String(err && err.message || err));
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function exportConfig() {
|
|
1266
|
+
if (!configState || !configState.config) { setNote('配置尚未载入'); return; }
|
|
1267
|
+
try {
|
|
1268
|
+
var blob = new Blob([JSON.stringify(configState.config, null, 2) + '\n'], { type: 'application/json' });
|
|
1269
|
+
var url = URL.createObjectURL(blob);
|
|
1270
|
+
var a = document.createElement('a');
|
|
1271
|
+
a.href = url;
|
|
1272
|
+
a.download = 'iris-prompt-optimizer.json';
|
|
1273
|
+
a.click();
|
|
1274
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
|
1275
|
+
setNote('已导出当前 JSON 配置');
|
|
1276
|
+
} catch (_) {
|
|
1277
|
+
setNote('导出失败');
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function resetConfig() {
|
|
1282
|
+
if (!window.confirm('恢复 Iris 内置默认 Prompt、目标模板和会话模型路由?')) return;
|
|
1283
|
+
promptRequest('reset', {}).then(function (data) {
|
|
1284
|
+
publishConfig(data);
|
|
1285
|
+
setNote('已恢复 Iris 内置默认配置');
|
|
1286
|
+
}).catch(function (err) {
|
|
1287
|
+
setNote('重置失败:' + String(err && err.message || err));
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function disableEntry() {
|
|
1292
|
+
if (busy) return;
|
|
1293
|
+
promptRequest('enabled', { enabled: false }).then(function (data) {
|
|
1294
|
+
publishConfig(data);
|
|
1295
|
+
setOpen(false);
|
|
1296
|
+
}).catch(function (err) { setNote('关闭失败:' + String(err && err.message || err)); });
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (configState && configState.config && configState.config.enabled === false) return null;
|
|
1300
|
+
|
|
1301
|
+
var routeLabel = 'DSH 默认模型';
|
|
1302
|
+
if (configState && configState.config && configState.config.route.mode === 'fixed') {
|
|
1303
|
+
routeLabel = configState.config.route.provider + ' / ' + configState.config.route.model;
|
|
1304
|
+
} else if (selectedRoute) {
|
|
1305
|
+
routeLabel = selectedRoute.provider + ' / ' + selectedRoute.model;
|
|
1306
|
+
}
|
|
1307
|
+
var reasoningSetting = configState && configState.config && configState.config.generation && configState.config.generation.reasoningEffort || 'off-if-supported';
|
|
1308
|
+
var reasoningLabel = reasoningSetting === 'off-if-supported' ? '支持时关闭' : reasoningSetting === 'provider-default' ? '供应商默认' : reasoningSetting === 'inherit' ? '跟随会话' : reasoningSetting;
|
|
1309
|
+
var outputBudget = configState && configState.config && configState.config.generation && configState.config.generation.maxOutputTokens || 1200;
|
|
1310
|
+
|
|
1311
|
+
var panel = null;
|
|
1312
|
+
if (open) {
|
|
1313
|
+
panel = React.createElement(React.Fragment, {},
|
|
1314
|
+
React.createElement('div', { className: 'iris-po-backdrop', onClick: function () { setOpen(false); }, 'aria-hidden': 'true' }),
|
|
1315
|
+
React.createElement('div', { className: 'iris-po-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Iris 提示词优化', onClick: function (e) { e.stopPropagation(); } },
|
|
1316
|
+
React.createElement('div', { className: 'iris-po-head' },
|
|
1317
|
+
React.createElement('strong', { className: 'iris-po-title' }, React.createElement('span', { className: 'iris-po-mark' }, '🫧'), 'Iris 提示词优化'),
|
|
1318
|
+
React.createElement('button', { className: 'iris-po-close', onClick: function () { setOpen(false); }, title: '关闭' }, '✕')),
|
|
1319
|
+
React.createElement('div', { className: 'iris-po-muted' }, '只处理当前未发送草稿,不读取会话历史;调用模型可能产生费用。'),
|
|
1320
|
+
React.createElement('label', { className: 'iris-po-field' },
|
|
1321
|
+
React.createElement('span', {}, '优化目标'),
|
|
1322
|
+
React.createElement('select', { value: target, disabled: busy, onChange: function (e) { setTarget(e.target.value); } },
|
|
1323
|
+
React.createElement('option', { value: 'general' }, '通用对话'),
|
|
1324
|
+
React.createElement('option', { value: 'image' }, '图片生成'),
|
|
1325
|
+
React.createElement('option', { value: 'video' }, '视频生成'),
|
|
1326
|
+
React.createElement('option', { value: 's2v' }, '首尾帧视频'))),
|
|
1327
|
+
React.createElement('div', { className: 'iris-po-route', title: routeLabel }, '模型:' + routeLabel + ' · 思考:' + reasoningLabel + ' · 输出:' + outputBudget + ' tokens'),
|
|
1328
|
+
React.createElement('div', { className: 'iris-po-actions' },
|
|
1329
|
+
React.createElement('button', { className: 'iris-po-primary', disabled: busy || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: optimize }, busy ? '优化中…' : '生成预览'),
|
|
1330
|
+
busy ? React.createElement('button', { className: 'iris-po-btn', onClick: cancel }, '取消') : null),
|
|
1331
|
+
result ? React.createElement('div', { className: 'iris-po-result' },
|
|
1332
|
+
React.createElement('div', { className: 'iris-po-result-label' }, '优化结果'),
|
|
1333
|
+
React.createElement('textarea', { readOnly: true, value: result.optimized }),
|
|
1334
|
+
React.createElement('div', { className: 'iris-po-route' }, '实际使用:' + result.route.provider + ' / ' + result.route.model + ' · 思考:' + (result.route.reasoningEffort === 'provider-default' ? '供应商默认' : result.route.reasoningEffort)),
|
|
1335
|
+
React.createElement('div', { className: 'iris-po-actions' },
|
|
1336
|
+
React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(result.optimized); } }, '写回输入框'),
|
|
1337
|
+
React.createElement('button', { className: 'iris-po-btn', onClick: copyResult }, '复制'),
|
|
1338
|
+
React.createElement('button', { className: 'iris-po-btn', onClick: optimize }, '再优化'),
|
|
1339
|
+
React.createElement('button', { className: 'iris-po-btn', onClick: function () { replaceDraft(result.original); } }, '恢复原文'))) : null,
|
|
1340
|
+
note ? React.createElement('div', { className: 'iris-po-note' }, note) : null,
|
|
1341
|
+
React.createElement('details', { className: 'iris-po-config' },
|
|
1342
|
+
React.createElement('summary', {}, 'JSON 配置'),
|
|
1343
|
+
React.createElement('div', { className: 'iris-po-muted' }, '可导出后修改 systemPrompt、targets、route 和 generation,再重新导入。'),
|
|
1344
|
+
React.createElement('div', { className: 'iris-po-actions' },
|
|
1345
|
+
React.createElement('button', { className: 'iris-po-btn', onClick: exportConfig }, '导出'),
|
|
1346
|
+
React.createElement('button', { className: 'iris-po-btn', onClick: function () { if (fileRef.current) fileRef.current.click(); } }, '导入'),
|
|
1347
|
+
React.createElement('button', { className: 'iris-po-btn danger', onClick: resetConfig }, '恢复默认'),
|
|
1348
|
+
React.createElement('input', { ref: fileRef, className: 'iris-po-file', type: 'file', accept: 'application/json,.json', onChange: importConfig })),
|
|
1349
|
+
React.createElement('div', { className: 'iris-po-muted' }, '当前:' + (configState && configState.source === 'custom' ? '用户 JSON' : 'Iris 默认'))),
|
|
1350
|
+
React.createElement('button', { className: 'iris-po-disable', disabled: busy, onClick: disableEntry }, '关闭此对话入口(工作台仍保留)')));
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return React.createElement('div', { className: 'iris-po-wrap' },
|
|
1354
|
+
React.createElement('button', {
|
|
1355
|
+
className: 'iris-po-trigger',
|
|
1356
|
+
type: 'button',
|
|
1357
|
+
title: refCount ? '含结构化引用的草稿暂不支持优化' : '优化当前输入框提示词',
|
|
1358
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
1359
|
+
onClick: function () { setOpen(!open); }
|
|
1360
|
+
}, '🫧'),
|
|
1361
|
+
panel);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1030
1364
|
function ProgressDock() {
|
|
1031
1365
|
var state = useIrisState(5000);
|
|
1032
1366
|
var running = (state && state.tasks && state.tasks.running) || [];
|
|
@@ -1167,6 +1501,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1167
1501
|
}, WorkbenchPanel);
|
|
1168
1502
|
});
|
|
1169
1503
|
|
|
1504
|
+
ctx.slots.inject('conversation.input.right', function () {
|
|
1505
|
+
return ctx.slots.register({
|
|
1506
|
+
name: 'conversation.input.right',
|
|
1507
|
+
id: 'iris-prompt-optimizer',
|
|
1508
|
+
order: 40,
|
|
1509
|
+
label: function () { return 'Iris 提示词优化'; }
|
|
1510
|
+
}, PromptOptimizerControl);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1170
1513
|
ctx.slots.inject('conversation.input.dock', function () {
|
|
1171
1514
|
return ctx.slots.register({
|
|
1172
1515
|
name: 'conversation.input.dock',
|
package/lib/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { longOcr } from './ocr.js';
|
|
|
24
24
|
import { runAction, listActions } from './actions.js';
|
|
25
25
|
import { ffmpegAvailable, extractFrames, probeVideo, extractAudioTrack } from './media-probe.js';
|
|
26
26
|
import { buildContactSheet, summarizeMedia } from './summarize.js';
|
|
27
|
+
import { registerBundledSkills } from './bundled-skills.js';
|
|
27
28
|
|
|
28
29
|
const MEDIA_TYPES = {
|
|
29
30
|
'.png': 'image/png',
|
|
@@ -155,7 +156,7 @@ function fmtTask(t) {
|
|
|
155
156
|
return lines.join('\n');
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
export const name = 'dsh-iris';
|
|
159
|
+
export const name = 'mokuyoaxis-dsh-iris';
|
|
159
160
|
export const inject = ['tools'];
|
|
160
161
|
|
|
161
162
|
/**
|
|
@@ -214,6 +215,15 @@ export async function apply(ctx) {
|
|
|
214
215
|
// 若 webServer 已在运行,inject 回调也会触发;这里兜底立即挂载一次
|
|
215
216
|
guard('媒体路由兜底挂载', () => mountIrisRoutes(ctx));
|
|
216
217
|
|
|
218
|
+
// DSH 的 Skill registry 是可选宿主能力。存在时把 npm 包内两项 Skill 注册到
|
|
219
|
+
// 全局层;项目目录中的同名 Skill 仍由 DSH 原生优先级覆盖。
|
|
220
|
+
ctx.inject(['skills'], (skillCtx) => {
|
|
221
|
+
guard('随包 Skill 注册', () => {
|
|
222
|
+
const names = registerBundledSkills(skillCtx);
|
|
223
|
+
console.log('[iris] 随包 Skill 已注册:' + names.join(' · '));
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
217
227
|
const tools = ctx.tools;
|
|
218
228
|
if (!tools || typeof tools.register !== 'function') {
|
|
219
229
|
console.error('[iris] tools 服务不可用,跳过全部工具注册(宿主不受影响)');
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Iris 提示词优化器配置。
|
|
4
|
+
*
|
|
5
|
+
* 与 providers.json 分离保存:这里不含凭据,只记录用户可导入/导出的 Prompt、
|
|
6
|
+
* 目标媒介要求与模型路由偏好。损坏配置会被隔离,运行时回退到内置默认值。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { irisHome } from './config.js';
|
|
11
|
+
import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
|
|
12
|
+
|
|
13
|
+
export const PROMPT_TARGETS = ['general', 'image', 'video', 's2v'];
|
|
14
|
+
|
|
15
|
+
const DEFAULT_VALUE = {
|
|
16
|
+
version: 1,
|
|
17
|
+
enabled: true,
|
|
18
|
+
systemPrompt: [
|
|
19
|
+
'你是 Iris 提示词编辑器。把用户草稿改写成一段可直接交给目标 AI 的高质量提示词。',
|
|
20
|
+
'必须保留原始意图、事实、专有名词、引号内原文、硬性约束和否定条件;不得臆造用户没有提供的事实。',
|
|
21
|
+
'只补充能提高可执行性、结构清晰度、风格一致性和验收明确度的具体信息。信息不足时保持克制,不替用户作关键决定。',
|
|
22
|
+
'默认沿用草稿的语言。只输出优化后的提示词正文,不解释、不加标题、不使用 Markdown 代码块。'
|
|
23
|
+
].join('\n'),
|
|
24
|
+
targets: {
|
|
25
|
+
general: '适用于一般对话任务:明确目标、必要背景、约束、期望输出和验收标准;避免无意义地拉长文本。',
|
|
26
|
+
image: '适用于图像生成:在原意允许的范围内明确主体、环境、构图、视角、光线、色彩、材质、风格和负面约束。',
|
|
27
|
+
video: '适用于视频生成:在原意允许的范围内明确主体动作、场景变化、镜头运动、景别、节奏、时长感、连续性和负面约束。',
|
|
28
|
+
s2v: '适用于首尾帧视频:明确首帧到尾帧之间的动作、镜头、空间关系和连续性,避免引入与两端画面冲突的新主体。'
|
|
29
|
+
},
|
|
30
|
+
route: {
|
|
31
|
+
mode: 'session'
|
|
32
|
+
},
|
|
33
|
+
generation: {
|
|
34
|
+
temperature: 0.3,
|
|
35
|
+
reasoningEffort: 'off-if-supported',
|
|
36
|
+
maxOutputTokens: 1200,
|
|
37
|
+
timeoutMs: 45000
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_PROMPT_OPTIMIZER_CONFIG = Object.freeze(structuredClone(DEFAULT_VALUE));
|
|
42
|
+
|
|
43
|
+
function configFile() {
|
|
44
|
+
return path.join(irisHome(), 'prompt-optimizer.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function plain(value) {
|
|
48
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function text(value, fallback, field, maxLength) {
|
|
52
|
+
if (value === undefined) return fallback;
|
|
53
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error(`${field} 必须是非空字符串`);
|
|
54
|
+
if (value.length > maxLength) throw new Error(`${field} 不能超过 ${maxLength} 个字符`);
|
|
55
|
+
return value.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function boundedNumber(value, fallback, field, min, max) {
|
|
59
|
+
if (value === undefined) return fallback;
|
|
60
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
|
|
61
|
+
throw new Error(`${field} 必须是 ${min}–${max} 之间的数字`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 规范化用户导入的完整或局部 JSON,不保留未知字段。 */
|
|
67
|
+
export function normalizePromptOptimizerConfig(input) {
|
|
68
|
+
if (!plain(input)) throw new Error('提示词优化器配置根节点必须是对象');
|
|
69
|
+
if (input.version !== undefined && input.version !== 1) throw new Error('仅支持 version: 1');
|
|
70
|
+
|
|
71
|
+
const targetInput = input.targets === undefined ? {} : input.targets;
|
|
72
|
+
if (!plain(targetInput)) throw new Error('targets 必须是对象');
|
|
73
|
+
const targets = {};
|
|
74
|
+
for (const target of PROMPT_TARGETS) {
|
|
75
|
+
targets[target] = text(targetInput[target], DEFAULT_VALUE.targets[target], `targets.${target}`, 8000);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const routeInput = input.route === undefined ? {} : input.route;
|
|
79
|
+
if (!plain(routeInput)) throw new Error('route 必须是对象');
|
|
80
|
+
const mode = routeInput.mode === undefined ? DEFAULT_VALUE.route.mode : routeInput.mode;
|
|
81
|
+
if (mode !== 'session' && mode !== 'fixed') throw new Error('route.mode 只能是 session 或 fixed');
|
|
82
|
+
const route = { mode };
|
|
83
|
+
if (mode === 'fixed') {
|
|
84
|
+
route.provider = text(routeInput.provider, undefined, 'route.provider', 256);
|
|
85
|
+
route.model = text(routeInput.model, undefined, 'route.model', 256);
|
|
86
|
+
if (!route.provider || !route.model) throw new Error('fixed 路由必须同时提供 route.provider 与 route.model');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const generationInput = input.generation === undefined ? {} : input.generation;
|
|
90
|
+
if (!plain(generationInput)) throw new Error('generation 必须是对象');
|
|
91
|
+
const generation = {
|
|
92
|
+
temperature: boundedNumber(generationInput.temperature, DEFAULT_VALUE.generation.temperature, 'generation.temperature', 0, 2),
|
|
93
|
+
reasoningEffort: text(generationInput.reasoningEffort, DEFAULT_VALUE.generation.reasoningEffort, 'generation.reasoningEffort', 128),
|
|
94
|
+
maxOutputTokens: Math.round(boundedNumber(generationInput.maxOutputTokens, DEFAULT_VALUE.generation.maxOutputTokens, 'generation.maxOutputTokens', 64, 4096)),
|
|
95
|
+
timeoutMs: Math.round(boundedNumber(generationInput.timeoutMs, DEFAULT_VALUE.generation.timeoutMs, 'generation.timeoutMs', 1000, 120000))
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (input.enabled !== undefined && typeof input.enabled !== 'boolean') throw new Error('enabled 必须是布尔值');
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
version: 1,
|
|
102
|
+
enabled: input.enabled === undefined ? DEFAULT_VALUE.enabled : input.enabled,
|
|
103
|
+
systemPrompt: text(input.systemPrompt, DEFAULT_VALUE.systemPrompt, 'systemPrompt', 16000),
|
|
104
|
+
targets,
|
|
105
|
+
route,
|
|
106
|
+
generation
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cache = null;
|
|
111
|
+
let cacheSource = 'default';
|
|
112
|
+
|
|
113
|
+
export function loadPromptOptimizerConfig() {
|
|
114
|
+
if (cache) return { source: cacheSource, config: structuredClone(cache) };
|
|
115
|
+
const file = configFile();
|
|
116
|
+
if (!fs.existsSync(file)) {
|
|
117
|
+
cache = structuredClone(DEFAULT_VALUE);
|
|
118
|
+
cacheSource = 'default';
|
|
119
|
+
return { source: cacheSource, config: structuredClone(cache) };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
cache = normalizePromptOptimizerConfig(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
123
|
+
cacheSource = JSON.stringify(cache) === JSON.stringify(DEFAULT_VALUE) ? 'default' : 'custom';
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const backup = privateSibling(file, 'corrupted');
|
|
126
|
+
try {
|
|
127
|
+
fs.renameSync(file, backup);
|
|
128
|
+
chmodPrivateFile(backup);
|
|
129
|
+
} catch (_) { /* 隔离失败也必须能使用内置默认值 */ }
|
|
130
|
+
console.error('[iris] prompt-optimizer.json 已损坏,已回退内置默认值:', err && err.message);
|
|
131
|
+
cache = structuredClone(DEFAULT_VALUE);
|
|
132
|
+
cacheSource = 'default';
|
|
133
|
+
}
|
|
134
|
+
return { source: cacheSource, config: structuredClone(cache) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function importPromptOptimizerConfig(input) {
|
|
138
|
+
const next = normalizePromptOptimizerConfig(input);
|
|
139
|
+
atomicWritePrivate(configFile(), JSON.stringify(next, null, 2));
|
|
140
|
+
cache = next;
|
|
141
|
+
cacheSource = 'custom';
|
|
142
|
+
return { source: cacheSource, config: structuredClone(cache) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function setPromptOptimizerEnabled(enabled) {
|
|
146
|
+
if (typeof enabled !== 'boolean') throw new Error('enabled 必须是布尔值');
|
|
147
|
+
const next = normalizePromptOptimizerConfig({ ...loadPromptOptimizerConfig().config, enabled });
|
|
148
|
+
atomicWritePrivate(configFile(), JSON.stringify(next, null, 2));
|
|
149
|
+
cache = next;
|
|
150
|
+
cacheSource = JSON.stringify(cache) === JSON.stringify(DEFAULT_VALUE) ? 'default' : 'custom';
|
|
151
|
+
return { source: cacheSource, config: structuredClone(cache) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function resetPromptOptimizerConfig() {
|
|
155
|
+
const next = structuredClone(DEFAULT_VALUE);
|
|
156
|
+
atomicWritePrivate(configFile(), JSON.stringify(next, null, 2));
|
|
157
|
+
cache = next;
|
|
158
|
+
cacheSource = 'default';
|
|
159
|
+
return { source: cacheSource, config: structuredClone(cache) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 仅供测试/热重载丢弃进程内缓存。 */
|
|
163
|
+
export function resetPromptOptimizerConfigCache() {
|
|
164
|
+
cache = null;
|
|
165
|
+
cacheSource = 'default';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function promptOptimizerConfigFile() {
|
|
169
|
+
return configFile();
|
|
170
|
+
}
|