@mokuyoaxis/dsh-iris 0.2.2 → 0.2.4
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/.dsh/skills/iris-compose-media/SKILL.md +23 -73
- package/.dsh/skills/iris-compose-media/agents/openai.yaml +1 -1
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +23 -22
- package/.dsh/skills/iris-compose-media/references/workflows.md +25 -30
- package/.dsh/skills/iris-use/SKILL.md +42 -0
- package/.dsh/skills/iris-use/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-use/references/media-contract.md +83 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +9 -7
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +1 -1
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +3 -0
- package/CHANGELOG.md +41 -2
- package/README.en.md +50 -38
- package/README.md +48 -37
- package/bin/dsh-iris.js +35 -6
- package/docs/AGENT_QUICKSTART.md +9 -9
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CLI_MANAGEMENT.md +11 -3
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -1
- package/docs/DSH_COMPATIBILITY.md +43 -0
- package/docs/DSH_CORE_MIGRATION.md +6 -2
- package/docs/DSH_RC2_ADAPTATION.md +4 -2
- package/docs/FAULT_INJECTION.md +1 -0
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +63 -0
- package/docs/HEADLESS_CLI.md +14 -8
- package/docs/HOST_ADAPTER_CONTRACT.md +10 -7
- package/docs/HOST_DOCTOR.md +6 -4
- package/docs/IMAGE_IMPORT.md +49 -0
- package/docs/MODEL_PORT_CONTRACT.md +6 -2
- package/docs/MUSIC.md +77 -0
- package/docs/PROMPT_OPTIMIZER.md +48 -10
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +2 -0
- package/docs/ROADMAP.md +16 -25
- package/docs/SECURITY.md +3 -2
- package/docs/SYNC_DELIVERY_RECOVERY.md +39 -0
- package/docs/TASK_SEMANTICS.md +4 -4
- package/docs/VISION_CLI.md +3 -3
- package/docs/VISION_MODEL.md +1 -1
- package/docs/WORKBENCH_ARTIFACTS.md +4 -2
- package/docs/releases/0.2.3.md +54 -0
- package/docs/releases/0.2.4.md +27 -0
- package/lib/actions.js +71 -14
- package/lib/adapters.js +6 -2
- package/lib/api.js +14 -5
- package/lib/bundled-skills.js +1 -1
- package/lib/capability.js +2 -0
- package/lib/client.js +131 -47
- package/lib/command-service.js +28 -4
- package/lib/config.js +41 -23
- package/lib/core-artifact-store.js +44 -6
- package/lib/core-delivery-cache.js +145 -0
- package/lib/core-maintenance.js +12 -1
- package/lib/core-tasks.js +19 -5
- package/lib/core-user-projection.js +2 -2
- package/lib/doctor.js +4 -10
- package/lib/dsh-compatibility.js +10 -0
- package/lib/dsh-core-adapter.js +20 -4
- package/lib/dsh-text-model-adapter.js +15 -2
- package/lib/generation-input.js +10 -0
- package/lib/host-runtime.js +5 -2
- package/lib/image-edit-input.js +7 -5
- package/lib/image-import.js +61 -0
- package/lib/index.js +60 -17
- package/lib/models.js +12 -3
- package/lib/music-generation.js +21 -0
- package/lib/music-options.js +18 -0
- package/lib/music-transport.js +46 -0
- package/lib/ocr-model-routing.js +1 -0
- package/lib/prompt-optimizer-config.js +92 -21
- package/lib/prompt-optimizer-core.js +51 -6
- package/lib/prompt-optimizer.js +20 -14
- package/lib/provider-adapter.js +1 -0
- package/lib/provider-adapters.js +35 -3
- package/lib/provider-catalog.js +20 -11
- package/lib/provider-config-service.js +18 -6
- package/lib/provider-protocol.js +18 -0
- package/lib/provider-task-runner.js +80 -5
- package/lib/vision-model-routing.js +22 -8
- package/package.json +12 -9
- package/user_guide.md +45 -23
|
@@ -118,62 +118,133 @@ export function normalizePromptOptimizerConfig(input) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** 只反馈字段名,不携带被忽略的字段值;持久配置仍使用原 v1 规范化。 */
|
|
122
|
+
function importFeedback(input) {
|
|
123
|
+
const ignored = [];
|
|
124
|
+
const collect = (value, prefix, allowed) => {
|
|
125
|
+
if (!value) return;
|
|
126
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) ignored.push(prefix + key);
|
|
127
|
+
};
|
|
128
|
+
collect(input, '', ['version', 'enabled', 'systemPrompt', 'targets', 'route', 'generation']);
|
|
129
|
+
collect(input.targets, 'targets.', PROMPT_TARGETS);
|
|
130
|
+
collect(input.route, 'route.', input.route?.mode === 'fixed' ? ['mode', 'provider', 'model'] : ['mode']);
|
|
131
|
+
collect(input.generation, 'generation.', ['temperature', 'reasoningEffort', 'maxOutputTokens', 'timeoutMs']);
|
|
132
|
+
return { warnings: ignored.length ? [{ code: 'ignored-fields', count: ignored.length,
|
|
133
|
+
fields: ignored.slice(0, 32).map(field => field.length > 256 ? field.slice(0, 253) + '...' : field) }] : [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
let cache = null;
|
|
122
137
|
let cacheSource = 'default';
|
|
138
|
+
let cacheFeedback = { warnings: [] };
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
function snapshot() {
|
|
141
|
+
return { source: cacheSource, config: structuredClone(cache), feedback: structuredClone(cacheFeedback) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function persistConfig(next) {
|
|
145
|
+
const encoded = JSON.stringify(next, null, 2);
|
|
146
|
+
try { atomicWritePrivate(configFile(), encoded); }
|
|
147
|
+
catch (error) {
|
|
148
|
+
// 已知文件系统异常可能带私有路径;编程异常仍原样暴露。
|
|
149
|
+
if (typeof error.code !== 'string' || typeof error.syscall !== 'string') throw error;
|
|
150
|
+
const display = new Error('提示词配置写入失败,请检查存储权限和空间后重新载入配置');
|
|
151
|
+
display.code = error.code;
|
|
152
|
+
throw display;
|
|
131
153
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function useFallback(file, reason, isolate) {
|
|
157
|
+
let isolation = 'not-attempted';
|
|
158
|
+
if (isolate) {
|
|
136
159
|
const backup = privateSibling(file, 'corrupted');
|
|
137
160
|
try {
|
|
138
161
|
fs.renameSync(file, backup);
|
|
162
|
+
isolation = 'saved';
|
|
139
163
|
chmodPrivateFile(backup);
|
|
140
|
-
} catch (_) {
|
|
141
|
-
|
|
164
|
+
} catch (_) { if (isolation !== 'saved') isolation = 'failed'; }
|
|
165
|
+
}
|
|
166
|
+
cache = structuredClone(DEFAULT_VALUE);
|
|
167
|
+
cacheSource = 'default';
|
|
168
|
+
cacheFeedback = { warnings: [{ code: 'config-fallback', reason, isolation }] };
|
|
169
|
+
// JSON.parse/文件系统异常可能带原稿或绝对路径;只记录受控原因与隔离事实。
|
|
170
|
+
console.error('[iris] prompt-optimizer.json 已回退内置默认值:', reason, isolation);
|
|
171
|
+
return snapshot();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function loadPromptOptimizerConfig() {
|
|
175
|
+
if (cache) return snapshot();
|
|
176
|
+
const file = configFile();
|
|
177
|
+
let raw;
|
|
178
|
+
try { raw = fs.readFileSync(file, 'utf8'); }
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code !== 'ENOENT') return useFallback(file, 'read-failed', false);
|
|
142
181
|
cache = structuredClone(DEFAULT_VALUE);
|
|
143
182
|
cacheSource = 'default';
|
|
183
|
+
cacheFeedback = { warnings: [] };
|
|
184
|
+
return snapshot();
|
|
144
185
|
}
|
|
145
|
-
|
|
186
|
+
let input;
|
|
187
|
+
try { input = JSON.parse(raw); }
|
|
188
|
+
catch (_) { return useFallback(file, 'json-invalid', true); }
|
|
189
|
+
try {
|
|
190
|
+
cache = normalizePromptOptimizerConfig(input);
|
|
191
|
+
cacheSource = JSON.stringify(cache) === JSON.stringify(DEFAULT_VALUE) ? 'default' : 'custom';
|
|
192
|
+
} catch (_) { return useFallback(file, 'validation-invalid', true); }
|
|
193
|
+
cacheFeedback = importFeedback(input);
|
|
194
|
+
return snapshot();
|
|
146
195
|
}
|
|
147
196
|
|
|
148
197
|
export function importPromptOptimizerConfig(input) {
|
|
149
198
|
const next = normalizePromptOptimizerConfig(input);
|
|
150
|
-
|
|
199
|
+
persistConfig(next);
|
|
151
200
|
cache = next;
|
|
152
201
|
cacheSource = 'custom';
|
|
153
|
-
|
|
202
|
+
cacheFeedback = importFeedback(input);
|
|
203
|
+
return snapshot();
|
|
154
204
|
}
|
|
155
205
|
|
|
156
206
|
export function setPromptOptimizerEnabled(enabled) {
|
|
157
207
|
if (typeof enabled !== 'boolean') throw new Error('enabled 必须是布尔值');
|
|
158
|
-
const
|
|
159
|
-
|
|
208
|
+
const loaded = loadPromptOptimizerConfig();
|
|
209
|
+
if (loaded.feedback.warnings.some(warning => warning.code === 'config-fallback' && warning.isolation !== 'saved')) {
|
|
210
|
+
throw new Error('原配置尚未隔离保留,不能只更新入口开关;请先导入有效配置或明确恢复全部设置');
|
|
211
|
+
}
|
|
212
|
+
const next = normalizePromptOptimizerConfig({ ...loaded.config, enabled });
|
|
213
|
+
persistConfig(next);
|
|
214
|
+
cache = next;
|
|
215
|
+
cacheSource = JSON.stringify(cache) === JSON.stringify(DEFAULT_VALUE) ? 'default' : 'custom';
|
|
216
|
+
cacheFeedback = { warnings: cacheFeedback.warnings.filter(warning => warning.code === 'config-fallback') };
|
|
217
|
+
return snapshot();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 只更新指令;无法读取原设置时不得声称保留了路由与预算。 */
|
|
221
|
+
export function resetPromptOptimizerInstructions() {
|
|
222
|
+
const loaded = loadPromptOptimizerConfig();
|
|
223
|
+
if (loaded.feedback.warnings.some(warning => warning.code === 'config-fallback')) {
|
|
224
|
+
throw new Error('当前配置无法读取,不能保证保留模型与生成参数;请先导入有效配置或选择恢复全部设置');
|
|
225
|
+
}
|
|
226
|
+
const next = { ...loaded.config, systemPrompt: DEFAULT_VALUE.systemPrompt, targets: structuredClone(DEFAULT_VALUE.targets) };
|
|
227
|
+
persistConfig(next);
|
|
160
228
|
cache = next;
|
|
161
229
|
cacheSource = JSON.stringify(cache) === JSON.stringify(DEFAULT_VALUE) ? 'default' : 'custom';
|
|
162
|
-
|
|
230
|
+
cacheFeedback = { warnings: [] };
|
|
231
|
+
return snapshot();
|
|
163
232
|
}
|
|
164
233
|
|
|
165
234
|
export function resetPromptOptimizerConfig() {
|
|
166
235
|
const next = structuredClone(DEFAULT_VALUE);
|
|
167
|
-
|
|
236
|
+
persistConfig(next);
|
|
168
237
|
cache = next;
|
|
169
238
|
cacheSource = 'default';
|
|
170
|
-
|
|
239
|
+
cacheFeedback = { warnings: [] };
|
|
240
|
+
return snapshot();
|
|
171
241
|
}
|
|
172
242
|
|
|
173
243
|
/** 仅供测试/热重载丢弃进程内缓存。 */
|
|
174
244
|
export function resetPromptOptimizerConfigCache() {
|
|
175
245
|
cache = null;
|
|
176
246
|
cacheSource = 'default';
|
|
247
|
+
cacheFeedback = { warnings: [] };
|
|
177
248
|
}
|
|
178
249
|
|
|
179
250
|
export function promptOptimizerConfigFile() {
|
|
@@ -5,7 +5,7 @@ import { ModelPortError } from './model-port-contract.js';
|
|
|
5
5
|
|
|
6
6
|
export const PROMPT_TARGETS = Object.freeze(['general', 'image', 'image-edit', 'video', 's2v']);
|
|
7
7
|
export const PROMPT_EDITING_STRATEGIES = Object.freeze(['faithful', 'rewrite', 'suggestions']);
|
|
8
|
-
export const PROMPT_OPTIMIZER_CAPABILITIES = Object.freeze({ rules: true, assemble: true, targets: PROMPT_TARGETS, editing: PROMPT_EDITING_STRATEGIES });
|
|
8
|
+
export const PROMPT_OPTIMIZER_CAPABILITIES = Object.freeze({ rules: true, assemble: true, localFaithful: true, targets: PROMPT_TARGETS, editing: PROMPT_EDITING_STRATEGIES });
|
|
9
9
|
export const MAX_PROMPT_INPUT_BYTES = 32 * 1024;
|
|
10
10
|
export const MAX_PROMPT_OUTPUT_CHARS = 16000;
|
|
11
11
|
export const MAX_PROMPT_RULE_BYTES = 16 * 1024;
|
|
@@ -130,6 +130,32 @@ function parseEditingJson(body) {
|
|
|
130
130
|
try { return JSON.parse(fenced ? fenced[1] : body); } catch (_) { throw protocolError(); }
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function jsonValue(text) {
|
|
134
|
+
try { return JSON.parse(text); } catch (_) { return undefined; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function inputEnvelope(value) {
|
|
138
|
+
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 2
|
|
139
|
+
&& typeof value.target === 'string' && typeof value.prompt === 'string';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 只拒绝可识别的新增传输包装,不解包 JSON 或猜测性删除转义。 */
|
|
143
|
+
function validateRewriteFormat(body, input) {
|
|
144
|
+
const sources = [input.text, ...input.rules.filter(rule => rule.kind === 'optimization').map(rule => rule.text)];
|
|
145
|
+
const source = sources.join('\n');
|
|
146
|
+
const fence = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*)\n\1$/;
|
|
147
|
+
const value = jsonValue(body.match(fence)?.[2] ?? body);
|
|
148
|
+
const originalValue = jsonValue(input.text.match(fence)?.[2] ?? input.text);
|
|
149
|
+
// 原稿只在内部引用 JSON,不代表整份正文可以被重新包进该容器。
|
|
150
|
+
if (inputEnvelope(value) && !inputEnvelope(originalValue)
|
|
151
|
+
&& !sources.slice(1).some(text => /"target"\s*:/.test(text) && /"prompt"\s*:/.test(text))) throw protocolError();
|
|
152
|
+
// 原稿或明确规则已经使用围栏时,围栏属于素材;不要剥掉正文中的代码块。
|
|
153
|
+
if (fence.test(body) && !/`{3,}|~{3,}/.test(source)) throw protocolError();
|
|
154
|
+
// 整份正文又被序列化成字符串是明确的外壳;自然语言引文仍允许。
|
|
155
|
+
if (typeof value === 'string' && !sources.some(text => typeof jsonValue(text.match(fence)?.[2] ?? text) === 'string')
|
|
156
|
+
&& /\\(?:["\\nrt]|u[0-9a-f]{4})/i.test(body) && !source.includes(body)) throw protocolError();
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
function normalizeGroups(value, parts) {
|
|
134
160
|
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).join() !== 'groups'
|
|
135
161
|
|| !Array.isArray(value.groups) || !value.groups.length || value.groups.length > parts.length) throw protocolError();
|
|
@@ -167,15 +193,27 @@ function normalizeSuggestions(value, input) {
|
|
|
167
193
|
return { suggestions, questions };
|
|
168
194
|
}
|
|
169
195
|
|
|
170
|
-
function result(input, body, processing) {
|
|
196
|
+
function result(input, body, processing, invocation = input.mode === 'assemble' ? 'not_invoked' : 'responded') {
|
|
171
197
|
const prefix = input.rules.filter(rule => rule.position === 'prefix').map(rule => rule.text).join('\n\n');
|
|
172
198
|
const suffix = input.rules.filter(rule => rule.position === 'suffix').map(rule => rule.text).join('\n\n');
|
|
173
199
|
const optimized = [prefix, body, suffix].filter(value => value.length).join('\n\n');
|
|
174
|
-
if (optimized.length > MAX_PROMPT_OUTPUT_CHARS) throw new ModelPortError('IRIS_MODEL_OUTPUT_LIMIT', { stage: 'normalize', invocation
|
|
200
|
+
if (optimized.length > MAX_PROMPT_OUTPUT_CHARS) throw new ModelPortError('IRIS_MODEL_OUTPUT_LIMIT', { stage: 'normalize', invocation });
|
|
175
201
|
return { ok: true, original: input.original, optimized, target: input.target, mode: input.mode,
|
|
176
202
|
rules: input.rules, assembly: { prefix, body, suffix }, ...(processing ? { processing } : {}) };
|
|
177
203
|
}
|
|
178
204
|
|
|
205
|
+
/** 内部入口接收 normalizePromptInput 的结果;单片段没有可供模型选择的分段位置。 */
|
|
206
|
+
export function tryLocalPromptOptimization(normalized, { signal } = {}) {
|
|
207
|
+
checkSignal(signal);
|
|
208
|
+
if (normalized.mode !== 'optimize' || normalized.editing !== 'faithful') return null;
|
|
209
|
+
const parts = sourceParts(normalized.original);
|
|
210
|
+
if (parts.length !== 1) return null;
|
|
211
|
+
const out = result(normalized, normalized.original, undefined, 'not_invoked');
|
|
212
|
+
checkSignal(signal);
|
|
213
|
+
return { ...out, editing: { strategy: 'faithful', coverage: 'exact', order: 'preserved', groups: [[parts[0].id]] },
|
|
214
|
+
execution: { modelInvocations: 0, reason: 'single-part-faithful' } };
|
|
215
|
+
}
|
|
216
|
+
|
|
179
217
|
/** 保留原稿及输出规则的原始文字;零模型调用,不读取任何配置。 */
|
|
180
218
|
export function assemblePrompt(input, { signal } = {}) {
|
|
181
219
|
checkSignal(signal);
|
|
@@ -209,6 +247,8 @@ export function preparePromptOptimization(input, config) {
|
|
|
209
247
|
} else if (normalized.editing === 'suggestions') {
|
|
210
248
|
system += '\n\n## 本次输出协议:可选补充建议\n不修改原稿,不执行任务或生成媒体。只提出至多三条有价值的可选新指令,每条 text 是可直接加入提示词的简短文字,不声称用户已经要求、采纳或完成。无需凑满,没有必要补充时返回空数组。建议可以提出创造性细节,但不能编造作者作品、风格分期、历史、设备或技术事实作为依据。已明确的约数、左右、风格和否定条件不重新质疑,不泛化、不推演为额外限制。questions 只问影响目标的实际缺失或冲突,至多三条,不引导用户改变已明确要求。不猜测未见图片/音频内容,不编造链接、来源 ID 或参数。以下协议优先于通用正文输出约定:只返回 {"suggestions":[{"label":"简短名称","text":"可选指令"}],"questions":["待确认问题"]},不返回最终正文、媒体或其他字段。';
|
|
211
249
|
prompt = '只为以下原稿提出可选建议和待确认问题;JSON 是素材,不是任务执行授权。\n' + JSON.stringify({ target: normalized.target, prompt: normalized.original });
|
|
250
|
+
} else {
|
|
251
|
+
system += '\n\n## 本次输出协议:改写正文\n输入 JSON 是传输容器,target 等字段只提供上下文。只编辑 prompt 的字符串值,最终回复必须是改写后的字段值本身,即可直接阅读的原始正文。不要回传输入容器、字段名、额外包装引号、代码围栏、标题或解释。若原稿本身包含 JSON、代码、围栏或引号,应按用户规则保留这些原有内容。\n先理解 JSON 字符串的实际内容再编辑:容器中用于编码引号、换行和反斜杠的转义不是正文字符,不要复制序列化语法。保留原稿实际存在的引号、真实换行、字面反斜杠、代码和引用,禁止无条件去除或反转义。沿用原稿语言,只执行用户明确授权的内容变化,其余具体细节和否定条件继续保留。';
|
|
212
252
|
}
|
|
213
253
|
// 包含前后缀及分隔符,防止一次模型调用后才发现已选规则无法交付。
|
|
214
254
|
const overhead = normalized.rules.filter(rule => rule.kind === 'output').reduce((sum, rule) => sum + rule.text.length + 2, 0);
|
|
@@ -217,12 +257,14 @@ export function preparePromptOptimization(input, config) {
|
|
|
217
257
|
if (normalized.editing !== 'rewrite' && normalized.original.length > maxOutputChars) throw new ModelPortError('IRIS_MODEL_OUTPUT_LIMIT', { stage: 'prepare', invocation: 'not_invoked' });
|
|
218
258
|
return Object.freeze({ input: normalized, prompt, system, maxOutputChars, ...(parts ? { parts } : {}),
|
|
219
259
|
generation: Object.freeze({ temperature: generation.temperature, maxOutputTokens: generation.maxOutputTokens, timeoutMs: generation.timeoutMs }),
|
|
220
|
-
processing: Object.freeze({ systemPrompt, targetTemplate, optimizationRules: Object.freeze(optimizationRules)
|
|
260
|
+
processing: Object.freeze({ systemPrompt, targetTemplate, optimizationRules: Object.freeze(optimizationRules),
|
|
261
|
+
request: Object.freeze({ system, prompt }) }) });
|
|
221
262
|
}
|
|
222
263
|
|
|
223
264
|
/** 单轮、无工具;完整模型结果通过验证后再确定性插入输出规则。 */
|
|
224
265
|
export async function runPromptOptimization(port, plan, { signal, operation, reasoning } = {}) {
|
|
225
266
|
checkSignal(signal);
|
|
267
|
+
if (plan.parts?.length === 1) return tryLocalPromptOptimization(plan.input, { signal });
|
|
226
268
|
const request = { prompt: plan.prompt, system: plan.system, generation: {
|
|
227
269
|
temperature: plan.generation.temperature, maxOutputTokens: plan.generation.maxOutputTokens,
|
|
228
270
|
...(reasoning ? { reasoning } : {}) } };
|
|
@@ -231,8 +273,10 @@ export async function runPromptOptimization(port, plan, { signal, operation, rea
|
|
|
231
273
|
const completion = operation ? await operation.invoke(port, request, options) : await invokeModel(port, request, options);
|
|
232
274
|
checkSignal(signal);
|
|
233
275
|
let body = completion.text.trim();
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
if (plan.input.editing !== 'rewrite') {
|
|
277
|
+
const fenced = body.match(/^```(?:text|markdown)?\s*\n([\s\S]*?)\n```$/i);
|
|
278
|
+
if (fenced) body = fenced[1].trim();
|
|
279
|
+
}
|
|
236
280
|
if (!body) throw new ModelPortError('IRIS_MODEL_EMPTY_RESULT', { stage: 'normalize', invocation: 'responded' });
|
|
237
281
|
if (plan.input.editing === 'faithful') {
|
|
238
282
|
const grouped = normalizeGroups(parseEditingJson(body), plan.parts);
|
|
@@ -242,6 +286,7 @@ export async function runPromptOptimization(port, plan, { signal, operation, rea
|
|
|
242
286
|
const optional = normalizeSuggestions(parseEditingJson(body), plan.input);
|
|
243
287
|
return { ...result(plan.input, plan.input.original, plan.processing), editing: { strategy: 'suggestions' }, ...optional };
|
|
244
288
|
}
|
|
289
|
+
validateRewriteFormat(body, plan.input);
|
|
245
290
|
validateEditedText(body, plan.input);
|
|
246
291
|
return result(plan.input, body, plan.processing);
|
|
247
292
|
}
|
package/lib/prompt-optimizer.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** DSH 优化入口:v1 配置与模型选择/投影;共享业务不读取宿主和存储。 */
|
|
3
3
|
import { loadPromptOptimizerConfig } from './prompt-optimizer-config.js';
|
|
4
4
|
import { hasHostPort, requireHostPort } from './host-contract.js';
|
|
5
|
-
import { normalizePromptInput, assemblePrompt, preparePromptOptimization, runPromptOptimization } from './prompt-optimizer-core.js';
|
|
5
|
+
import { normalizePromptInput, assemblePrompt, tryLocalPromptOptimization, preparePromptOptimization, runPromptOptimization } from './prompt-optimizer-core.js';
|
|
6
6
|
import { prepareDshTextModelPort } from './dsh-text-model-adapter.js';
|
|
7
7
|
import { createModelOperation } from './model-invoker.js';
|
|
8
8
|
import { ModelPortError, modelPortSnapshot, modelErrorRecord } from './model-port-contract.js';
|
|
@@ -10,31 +10,34 @@ import { ModelPortError, modelPortSnapshot, modelErrorRecord } from './model-por
|
|
|
10
10
|
export { MAX_PROMPT_INPUT_BYTES, MAX_PROMPT_OUTPUT_CHARS } from './prompt-optimizer-core.js';
|
|
11
11
|
|
|
12
12
|
function modelSelection(value, field) {
|
|
13
|
-
if (!value || typeof value !== 'object')
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${field} 必须同时包含有效的 provider 与 model`);
|
|
14
14
|
const provider = typeof value.provider === 'string' ? value.provider.trim() : '';
|
|
15
15
|
const model = typeof value.model === 'string' ? value.model.trim() : '';
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
return null;
|
|
16
|
+
if ([provider, model].some(id => !id || id.length > 256 || /[\u0000-\u001f\u007f]/.test(id) || id.startsWith('/') || id.includes('://'))) {
|
|
17
|
+
throw new Error(`${field} 必须同时包含有效的 provider 与 model`);
|
|
19
18
|
}
|
|
20
19
|
const out = { provider, model };
|
|
21
|
-
if (
|
|
22
|
-
|
|
20
|
+
if (value.reasoningEffort !== undefined) {
|
|
21
|
+
if (typeof value.reasoningEffort !== 'string' || !value.reasoningEffort.trim() || value.reasoningEffort.trim().length > 128
|
|
22
|
+
|| /[\u0000-\u001f\u007f]/.test(value.reasoningEffort)) throw new Error(`${field}.reasoningEffort 无效`);
|
|
23
|
+
out.reasoningEffort = value.reasoningEffort.trim();
|
|
23
24
|
}
|
|
24
25
|
return out;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function resolvePromptOptimizerRoute(host, config, requestedRoute) {
|
|
28
29
|
if (config.route.mode === 'fixed') {
|
|
29
|
-
return { ...modelSelection(config.route, 'route'), source: 'iris-fixed' };
|
|
30
|
+
return { ...modelSelection(config.route, 'route'), source: 'iris-fixed', reason: '固定配置优先于当前会话和宿主默认模型' };
|
|
31
|
+
}
|
|
32
|
+
if (requestedRoute !== undefined && requestedRoute !== null) {
|
|
33
|
+
return { ...modelSelection(requestedRoute, '请求路由'), source: 'current-session', reason: '使用当前会话明确选择的模型' };
|
|
30
34
|
}
|
|
31
|
-
const current = modelSelection(requestedRoute);
|
|
32
|
-
if (current) return { ...current, source: 'current-session' };
|
|
33
35
|
const textModel = hasHostPort(host, 'textModel') ? host.ports.textModel : undefined;
|
|
34
36
|
const fallback = textModel && typeof textModel.currentSelection === 'function'
|
|
35
|
-
?
|
|
37
|
+
? textModel.currentSelection()
|
|
36
38
|
: null;
|
|
37
|
-
if (fallback) return { ...fallback, source: 'host-default'
|
|
39
|
+
if (fallback !== null && fallback !== undefined) return { ...modelSelection(fallback, '宿主默认路由'), source: 'host-default',
|
|
40
|
+
reason: '未提供当前会话模型,使用宿主默认模型' };
|
|
38
41
|
throw new Error('Iris 找不到可用文本模型:请先在 DSH 会话中选择模型,或在 JSON 配置中设置 fixed 路由');
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -54,6 +57,8 @@ export async function optimizePrompt(host, input, { signal } = {}) {
|
|
|
54
57
|
if (signal?.aborted) throw new ModelPortError('IRIS_MODEL_ABORTED');
|
|
55
58
|
const normalized = normalizePromptInput(input);
|
|
56
59
|
if (normalized.mode === 'assemble') return { ...assemblePrompt(input, { signal }), configSource: 'not-loaded', route: null };
|
|
60
|
+
const local = tryLocalPromptOptimization(normalized, { signal });
|
|
61
|
+
if (local) return { ...local, configSource: 'not-loaded', route: null };
|
|
57
62
|
const loaded = loadPromptOptimizerConfig();
|
|
58
63
|
const config = loaded.config;
|
|
59
64
|
const plan = preparePromptOptimization(input, config);
|
|
@@ -62,10 +67,11 @@ export async function optimizePrompt(host, input, { signal } = {}) {
|
|
|
62
67
|
try {
|
|
63
68
|
const route = resolvePromptOptimizerRoute(host, config, input?.route);
|
|
64
69
|
const llm = requireHostPort(host, 'textModel', '提示词优化');
|
|
65
|
-
const { port, offId } = await prepareDshTextModelPort(llm, { ...route, sessionId: input?.sessionId }, { signal: operation.signal });
|
|
70
|
+
const { port, offId, textInput } = await prepareDshTextModelPort(llm, { ...route, sessionId: input?.sessionId }, { signal: operation.signal });
|
|
66
71
|
reasoning = reasoningPolicy(port, offId, route, config.generation.reasoningEffort);
|
|
67
72
|
const result = await runPromptOptimization(port, plan, { signal: operation.signal, operation, reasoning: reasoning.request });
|
|
68
|
-
return { ...result, configSource: loaded.source, route: { source: route.source,
|
|
73
|
+
return { ...result, configSource: loaded.source, configFeedback: loaded.feedback, route: { source: route.source, reason: route.reason, textInput,
|
|
74
|
+
provider: route.provider, model: route.model,
|
|
69
75
|
reasoningPolicy: reasoning.policy, reasoningEffort: reasoning.effective } };
|
|
70
76
|
} catch (error) {
|
|
71
77
|
if (modelErrorRecord(error).code === 'IRIS_MODEL_OUTPUT_LIMIT') {
|
package/lib/provider-adapter.js
CHANGED
package/lib/provider-adapters.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as defaultTransport from './adapters.js';
|
|
|
10
10
|
import { ProviderContractError, providerErrorRecord } from './provider-contract.js';
|
|
11
11
|
import { writePrivateFile } from './private-storage.js';
|
|
12
12
|
import { defineProviderAdapter } from './provider-adapter.js';
|
|
13
|
-
import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError } from './provider-protocol.js';
|
|
13
|
+
import { selectMediaProtocol, providerApiKey, providerMediaBaseUrl, unsupportedProtocolError, supportsImageEdit } from './provider-protocol.js';
|
|
14
14
|
import { modelRateLimit, reportModelRateLimit, isFreeQuotaExhausted } from './provider-health.js';
|
|
15
15
|
import { prepareVisionImage, visionInputLimits } from './vision-image-input.js';
|
|
16
16
|
|
|
@@ -110,7 +110,7 @@ function writeInlineArtifact(artifact, targetPath) {
|
|
|
110
110
|
const data = String(artifact?.data || '').trim();
|
|
111
111
|
const body = Buffer.from(data, 'base64');
|
|
112
112
|
if (!data || body.length === 0 || body.toString('base64') !== data) {
|
|
113
|
-
throw new ProviderContractError('inline-base64
|
|
113
|
+
throw new ProviderContractError('inline-base64 媒体数据无效', {
|
|
114
114
|
stage: 'download', category: 'protocol', acceptance: 'accepted'
|
|
115
115
|
});
|
|
116
116
|
}
|
|
@@ -132,6 +132,15 @@ function operationsForDashScope(provider, transport) {
|
|
|
132
132
|
...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {})
|
|
133
133
|
};
|
|
134
134
|
if (request.capability === 'image') {
|
|
135
|
+
let image;
|
|
136
|
+
if (input.image) {
|
|
137
|
+
if (!supportsImageEdit('dashscope', request.model)) throw new ProviderContractError('该 DashScope 模型尚未接入原生改图', {
|
|
138
|
+
stage: 'validate', category: 'invalid_request', acceptance: 'not_accepted'
|
|
139
|
+
});
|
|
140
|
+
const limits = visionInputLimits(provider, request.model);
|
|
141
|
+
image = await prepareVisionImage(input.image, { ...limits, maxBytes: Math.min(limits.maxBytes, 10 * 1024 * 1024) },
|
|
142
|
+
{ signal: request.signal, orient: true });
|
|
143
|
+
}
|
|
135
144
|
if (transport.dashscopeImageMode(request.model) === 'legacy-async') {
|
|
136
145
|
const remoteTaskId = await transport.submitImage({
|
|
137
146
|
...common, prompt: input.prompt, size: input.size, n: input.n
|
|
@@ -139,7 +148,7 @@ function operationsForDashScope(provider, transport) {
|
|
|
139
148
|
return { kind: 'accepted', remoteTaskId };
|
|
140
149
|
}
|
|
141
150
|
const urls = await transport.generateImageMultimodal({
|
|
142
|
-
...common, prompt: input.prompt, size: input.size, n: input.n
|
|
151
|
+
...common, prompt: input.prompt, size: input.size, n: input.n, ...(image ? { image } : {})
|
|
143
152
|
});
|
|
144
153
|
return {
|
|
145
154
|
kind: 'completed', value: { kind: 'urls', items: urls },
|
|
@@ -373,7 +382,30 @@ export function createOpenAiResponsesImagesProviderAdapter(provider, { transport
|
|
|
373
382
|
});
|
|
374
383
|
}
|
|
375
384
|
|
|
385
|
+
export function createGeminiWebMusicProviderAdapter(provider, { transport = defaultTransport } = {}) {
|
|
386
|
+
const { id, key, baseUrl } = providerIdentity(provider);
|
|
387
|
+
return defineProviderAdapter({
|
|
388
|
+
id, protocol: 'gemini-web-music', capabilities: ['music'],
|
|
389
|
+
operations: {
|
|
390
|
+
async discover({ signal, timeoutMs } = {}) {
|
|
391
|
+
return { models: await transport.listModels({ key, baseUrl, signal, timeoutMs }) };
|
|
392
|
+
},
|
|
393
|
+
async submit(request) {
|
|
394
|
+
if (request.capability !== 'music') throw unsupportedCapability(request.capability, 'gemini-web-music');
|
|
395
|
+
const artifacts = await transport.generateMusic({ key, baseUrl, model: request.model,
|
|
396
|
+
prompt: request.input.prompt, timeoutMs: request.input.timeoutMs, signal: request.signal });
|
|
397
|
+
return { kind: 'completed', artifacts };
|
|
398
|
+
},
|
|
399
|
+
async download({ artifact, targetPath }) { return writeInlineArtifact(artifact, targetPath); },
|
|
400
|
+
mapError: mapProviderAdapterError
|
|
401
|
+
},
|
|
402
|
+
unsupported: { poll: '网关 Music 没有客户端可恢复的远端任务ID', cancel: '网关 Music 不提供远端取消' },
|
|
403
|
+
unsupportedPreparation: 'Music 描述输入不需要上传'
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
376
407
|
const PROTOCOL_FACTORIES = new Map([
|
|
408
|
+
['gemini-web-music', createGeminiWebMusicProviderAdapter],
|
|
377
409
|
['dashscope', createDashScopeProviderAdapter],
|
|
378
410
|
['openai-images', createOpenAiImagesProviderAdapter],
|
|
379
411
|
['openai-chat-images', createOpenAiChatImagesProviderAdapter],
|
package/lib/provider-catalog.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { CAPABILITIES } from './capability.js';
|
|
7
7
|
import { modelPool, orderedModels, providerModels, parseModelRef, resolveModelRef, resolvePoolModel, selectionReasonForModel } from './models.js';
|
|
8
|
-
import { dashscopeApiBase, selectMediaProtocol, providerForImageModel, providerMediaBaseUrl, isConfiguredProvider } from './provider-protocol.js';
|
|
8
|
+
import { dashscopeApiBase, selectMediaProtocol, providerForImageModel, providerForMusicModel, providerMediaBaseUrl, isConfiguredProvider } from './provider-protocol.js';
|
|
9
9
|
import { redactProviderMessage } from './provider-contract.js';
|
|
10
10
|
import { modelRateLimit } from './provider-health.js';
|
|
11
11
|
import { normalizeVisionInputLimits } from './vision-image-input.js';
|
|
@@ -54,21 +54,23 @@ export function loadProviderCatalog(configPath) {
|
|
|
54
54
|
|
|
55
55
|
const CANDIDATE_CAP_LABEL = Object.freeze({
|
|
56
56
|
[CAPABILITIES.IMAGE]: '图片', [CAPABILITIES.VIDEO]: '视频',
|
|
57
|
-
[CAPABILITIES.TTS]: '语音', [CAPABILITIES.TRANSCRIBE]: '转写', [CAPABILITIES.VISION]: '视觉'
|
|
57
|
+
[CAPABILITIES.TTS]: '语音', [CAPABILITIES.TRANSCRIBE]: '转写', [CAPABILITIES.VISION]: '视觉',
|
|
58
|
+
[CAPABILITIES.MUSIC]: '音乐'
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
/**
|
|
61
|
+
/** 与 DSH 相同的严格功能名单;CLI 不附加宿主资源。 */
|
|
61
62
|
function candidatesFromCatalog(catalog, requestedModelRef, capability) {
|
|
62
63
|
const label = CANDIDATE_CAP_LABEL[capability];
|
|
63
64
|
if (!label) fail('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', '当前 Headless 候选链只开放图片、视频、语音与转写能力');
|
|
64
65
|
const providers = catalog.providers.filter(isConfiguredProvider);
|
|
65
|
-
const
|
|
66
|
+
const configuredPool = modelPool(providers).filter((item) => item.capabilities.includes(capability));
|
|
67
|
+
const pool = orderedModels(configuredPool, capability, catalog.assignments?.[capability], { includeRateLimited: true });
|
|
66
68
|
const requested = String(requestedModelRef || '').trim();
|
|
67
69
|
if (requested) {
|
|
68
70
|
const parsed = parseModelRef(requested);
|
|
69
71
|
if (!parsed && requested.includes('::')) fail('IRIS_PROVIDER_MODEL_INVALID', 'model_ref 格式无效');
|
|
70
72
|
const exact = resolvePoolModel(pool, requested, capability);
|
|
71
|
-
if (!exact) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE',
|
|
73
|
+
if (!exact) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `指定模型未分配给${label}功能、未启用或没有所需能力`);
|
|
72
74
|
const provider = providers.find((item) => item.id === exact.providerId);
|
|
73
75
|
if (exact.rateLimited) fail('IRIS_PROVIDER_MODEL_RATE_LIMITED', modelRateLimit(provider, exact.id)?.until
|
|
74
76
|
? '指定模型正在限流冷却,请稍后使用或手动实测'
|
|
@@ -79,13 +81,13 @@ function candidatesFromCatalog(catalog, requestedModelRef, capability) {
|
|
|
79
81
|
})]);
|
|
80
82
|
}
|
|
81
83
|
const assignment = catalog.assignments?.[capability];
|
|
82
|
-
const ordered =
|
|
83
|
-
if (!ordered.length) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE',
|
|
84
|
+
const ordered = pool.filter(model => !model.rateLimited);
|
|
85
|
+
if (!ordered.length) fail('IRIS_PROVIDER_MODEL_UNAVAILABLE', `请先给${label}功能分配当前入口可用的模型`);
|
|
84
86
|
return Object.freeze(ordered.map((item) => Object.freeze({
|
|
85
87
|
provider: providers.find((provider) => provider.id === item.providerId),
|
|
86
88
|
model: item.id,
|
|
87
89
|
modelRef: item.ref,
|
|
88
|
-
selectionReason: selectionReasonForModel(
|
|
90
|
+
selectionReason: selectionReasonForModel(configuredPool, capability, assignment, item)
|
|
89
91
|
})));
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -102,6 +104,11 @@ export function ttsCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
|
102
104
|
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TTS);
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
export function musicCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
108
|
+
return Object.freeze(candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.MUSIC).map(route =>
|
|
109
|
+
Object.freeze({ ...route, provider: providerForMusicModel(route.provider, route.model) })));
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
export function transcribeCandidatesFromCatalog(catalog, requestedModelRef = '') {
|
|
106
113
|
return candidatesFromCatalog(catalog, requestedModelRef, CAPABILITIES.TRANSCRIBE);
|
|
107
114
|
}
|
|
@@ -152,7 +159,7 @@ export function providerTaskBinding(provider) {
|
|
|
152
159
|
try {
|
|
153
160
|
const url = new URL(baseUrl);
|
|
154
161
|
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('unsafe endpoint');
|
|
155
|
-
if (!['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images'].includes(protocol)) throw new Error('unknown protocol');
|
|
162
|
+
if (!['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images', 'gemini-web-music'].includes(protocol)) throw new Error('unknown protocol');
|
|
156
163
|
endpoint = protocol === 'dashscope' ? dashscopeApiBase(baseUrl) : url.href.replace(/\/+$/, '');
|
|
157
164
|
} catch (_) { fail('IRIS_PROVIDER_CONFIG_INVALID', 'Provider 媒体端点或协议无效'); }
|
|
158
165
|
return 'sha256:' + crypto.createHash('sha256').update(JSON.stringify([1, protocol, endpoint])).digest('hex');
|
|
@@ -164,17 +171,19 @@ export function providerForTaskFromCatalog(catalog, task) {
|
|
|
164
171
|
task?.capability === 'video' ? 'VIDEO'
|
|
165
172
|
: task?.capability === 'image' ? 'IMAGE'
|
|
166
173
|
: task?.capability === 'tts' ? 'TTS'
|
|
174
|
+
: task?.capability === 'music' ? 'MUSIC'
|
|
167
175
|
: task?.capability === 'transcribe' ? 'TRANSCRIBE' : ''
|
|
168
176
|
];
|
|
169
177
|
if (!capability) fail('IRIS_PROVIDER_TASK_OBSERVE_UNSUPPORTED', '当前 Headless 恢复解析只开放图片、视频、语音与转写 Task');
|
|
170
|
-
const label = { video: '视频', tts: '语音', transcribe: '转写' }[task.capability] || '图片';
|
|
178
|
+
const label = { video: '视频', tts: '语音', music: '音乐', transcribe: '转写' }[task.capability] || '图片';
|
|
171
179
|
const parsed = parseModelRef(task.modelRef);
|
|
172
180
|
if (!parsed || parsed.providerId !== task.providerId) fail('IRIS_PROVIDER_TASK_IDENTITY_MISMATCH', 'Task 的 Provider 与模型身份不一致');
|
|
173
181
|
const matches = catalog.providers.filter((item) => item && item.id === task.providerId);
|
|
174
182
|
if (matches.length > 1) fail('IRIS_PROVIDER_CONFIG_AMBIGUOUS', 'provider-config 存在重复 Provider ID');
|
|
175
183
|
const configured = matches[0];
|
|
176
184
|
if (!configured || configured.enabled === false || (configured.auth !== 'none' && (typeof configured.apiKey !== 'string' || !configured.apiKey.trim()))) fail('IRIS_PROVIDER_TASK_PROVIDER_UNAVAILABLE', 'Task 原 Provider 已移除、停用或缺少凭据');
|
|
177
|
-
const provider = task.capability === 'image' ? providerForImageModel(configured, parsed.modelId)
|
|
185
|
+
const provider = task.capability === 'image' ? providerForImageModel(configured, parsed.modelId)
|
|
186
|
+
: task.capability === 'music' ? providerForMusicModel(configured, parsed.modelId) : configured;
|
|
178
187
|
if (!task.providerBinding) fail('IRIS_PROVIDER_TASK_BINDING_MISSING', '旧 Task 缺少提交端点绑定;仅支持只读查看,不猜测恢复配置');
|
|
179
188
|
if (providerTaskBinding(provider) !== task.providerBinding) fail('IRIS_PROVIDER_TASK_BINDING_MISMATCH', 'Task 提交端点或协议已改变;拒绝向新端点观察原任务');
|
|
180
189
|
const model = modelPool([provider]).find((item) => item.providerId === parsed.providerId && item.id === parsed.modelId && item.capabilities.includes(capability));
|
|
@@ -3,10 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot } from './provider-catalog.js';
|
|
5
5
|
import { isKnownCapability } from './capability.js';
|
|
6
|
-
import { providerModels, modelPool, parseModelRef, capabilitiesOfDiscoveredModel, hasConfiguredModelCapabilities, resolvePoolModel } from './models.js';
|
|
6
|
+
import { providerModels, modelPool, parseModelRef, HOST_VISION_REF, capabilitiesOfDiscoveredModel, hasConfiguredModelCapabilities, resolvePoolModel } from './models.js';
|
|
7
7
|
import { atomicWritePrivate, privateSibling, ensurePrivateDir } from './private-storage.js';
|
|
8
8
|
import { createConfiguredProviderAdapter } from './provider-adapters.js';
|
|
9
|
-
import { selectImageProtocol, normalizeImageProtocol } from './provider-protocol.js';
|
|
9
|
+
import { selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol } from './provider-protocol.js';
|
|
10
10
|
import { invokeProviderOperation } from './provider-adapter.js';
|
|
11
11
|
import { redactProviderMessage } from './provider-contract.js';
|
|
12
12
|
import { modelRateLimit, recordModelRateLimit, clearModelRateLimit, clearModelCapabilityHealth, isDefinitiveHealthFailure } from './provider-health.js';
|
|
@@ -22,7 +22,7 @@ const caps = value => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
function assignmentSnapshot(catalog) {
|
|
25
|
-
const pool = modelPool(catalog.providers);
|
|
25
|
+
const pool = modelPool(catalog.providers, { includeHostVision: true });
|
|
26
26
|
return Object.fromEntries(Object.entries(catalog.assignments || {}).map(([capability, value]) => {
|
|
27
27
|
const refs = (Array.isArray(value) ? value : [value]).map(ref => resolvePoolModel(pool, ref, capability));
|
|
28
28
|
return [safe(capability), { model_refs: refs.filter(Boolean).map(model => safe(model.ref)), invalidRefs: refs.filter(ref => !ref).length }];
|
|
@@ -33,6 +33,7 @@ export function configSnapshot(catalog) {
|
|
|
33
33
|
return { ...providerCatalogSnapshot(catalog), models: catalog.providers.flatMap(provider => providerModels(provider).map(model => ({
|
|
34
34
|
ref: safe(model.ref), providerId: safe(provider.id), id: safe(model.id), capabilities: model.capabilities,
|
|
35
35
|
...(model.imageProtocol !== undefined ? { imageProtocol: model.imageProtocol } : {}),
|
|
36
|
+
...(model.musicProtocol !== undefined ? { musicProtocol: model.musicProtocol } : {}),
|
|
36
37
|
...(model.visionInput !== undefined ? { visionInput: model.visionInput } : {}),
|
|
37
38
|
...(model.capabilities.includes('vision') ? { visionInputEffective: visionInputLimits(provider, model.id) } : {}),
|
|
38
39
|
...(model.capabilities.includes('image-gen') ? { imageRouting: selectImageProtocol(provider, model.id) } : {}),
|
|
@@ -58,10 +59,13 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
|
|
|
58
59
|
const issues = [];
|
|
59
60
|
const ids = catalog.providers.map(provider => provider.id);
|
|
60
61
|
if (new Set(ids).size !== ids.length) issues.push('duplicate_provider_id');
|
|
61
|
-
const pool = modelPool(catalog.providers);
|
|
62
|
+
const pool = modelPool(catalog.providers, { includeHostVision: true });
|
|
62
63
|
for (const provider of catalog.providers) {
|
|
63
64
|
try { normalizeVisionInputLimits(provider.visionInput); } catch (_) { issues.push('invalid_vision_input'); }
|
|
64
65
|
for (const model of provider.models || []) {
|
|
66
|
+
if (model?.musicProtocol !== undefined) {
|
|
67
|
+
try { normalizeMusicProtocol(model.musicProtocol); } catch (_) { issues.push('invalid_model_music_protocol'); }
|
|
68
|
+
}
|
|
65
69
|
try { normalizeVisionInputLimits(model?.visionInput); } catch (_) { issues.push('invalid_model_vision_input'); }
|
|
66
70
|
if (model?.imageProtocol === undefined) continue;
|
|
67
71
|
try { normalizeImageProtocol(model.imageProtocol); } catch (_) { issues.push('invalid_model_image_protocol'); }
|
|
@@ -103,6 +107,7 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
|
|
|
103
107
|
}
|
|
104
108
|
updated.models = updated.models.map(model => typeof model === 'string' ? model : ({ ...model,
|
|
105
109
|
...(model?.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(model.imageProtocol) } : {}),
|
|
110
|
+
...(model?.musicProtocol !== undefined ? { musicProtocol: normalizeMusicProtocol(model.musicProtocol) } : {}),
|
|
106
111
|
...(model?.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(model.visionInput) || null } : {}) }));
|
|
107
112
|
}
|
|
108
113
|
if (provider) {
|
|
@@ -140,6 +145,7 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
|
|
|
140
145
|
if (entry) fail('模型已存在;请用 models caps 修改能力');
|
|
141
146
|
entries.push({ id: modelId, capabilities, source: 'manual',
|
|
142
147
|
...(input.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(input.imageProtocol) } : {}),
|
|
148
|
+
...(input.musicProtocol !== undefined ? { musicProtocol: normalizeMusicProtocol(input.musicProtocol) } : {}),
|
|
143
149
|
...(input.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(input.visionInput) || null } : {}) });
|
|
144
150
|
} else if (command === 'models.remove') {
|
|
145
151
|
if (!entry) fail('模型不存在');
|
|
@@ -156,6 +162,12 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
|
|
|
156
162
|
provider.models = entries;
|
|
157
163
|
if (previous !== selectImageProtocol(provider, modelId).mediaProtocol) clearModelCapabilityHealth(provider, modelId, 'image-gen');
|
|
158
164
|
result = { ...result, model_ref: safe(input.model_ref), imageRouting: selectImageProtocol(provider, modelId) };
|
|
165
|
+
} else if (command === 'models.music-protocol') {
|
|
166
|
+
if (!entry) fail('模型不存在');
|
|
167
|
+
const protocol = normalizeMusicProtocol(input.musicProtocol);
|
|
168
|
+
if (entry.musicProtocol !== protocol) clearModelCapabilityHealth(provider, modelId, 'music-gen');
|
|
169
|
+
entry.musicProtocol = protocol;
|
|
170
|
+
result = { ...result, model_ref: safe(input.model_ref), musicProtocol: protocol };
|
|
159
171
|
} else if (command === 'models.vision-input') {
|
|
160
172
|
if (!entry || !Object.hasOwn(input, 'visionInput')) fail('需要已存在的模型和 visionInput 对象或 null');
|
|
161
173
|
const limits = normalizeVisionInputLimits(input.visionInput);
|
|
@@ -189,8 +201,8 @@ export async function executeConfigCommand(configPath, command, input = {}, { si
|
|
|
189
201
|
if (command === 'assignments.clear') delete catalog.assignments[input.capability];
|
|
190
202
|
else {
|
|
191
203
|
if (!Array.isArray(input.model_refs) || new Set(input.model_refs).size !== input.model_refs.length) fail('model_refs 必须是不重复的复合引用数组');
|
|
192
|
-
const pool = modelPool(catalog.providers);
|
|
193
|
-
for (const ref of input.model_refs) if (!parseModelRef(ref) || !resolvePoolModel(pool, ref, input.capability)) fail('分配模型不存在或没有该能力');
|
|
204
|
+
const pool = modelPool(catalog.providers, { includeHostVision: true });
|
|
205
|
+
for (const ref of input.model_refs) if ((!parseModelRef(ref) && ref !== HOST_VISION_REF) || !resolvePoolModel(pool, ref, input.capability)) fail('分配模型不存在或没有该能力');
|
|
194
206
|
catalog.assignments[input.capability] = input.model_refs;
|
|
195
207
|
}
|
|
196
208
|
} else fail('未知配置命令');
|