@mokuyoaxis/dsh-iris 0.2.1 → 0.2.3
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 +41 -0
- package/.dsh/skills/iris-use/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-use/references/media-contract.md +73 -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 +48 -2
- package/README.en.md +47 -40
- package/README.md +45 -39
- package/bin/dsh-iris.js +27 -6
- package/docs/AGENT_QUICKSTART.md +9 -9
- package/docs/ARCHITECTURE.md +11 -3
- package/docs/ARTIFACT_LIBRARY.md +1 -1
- package/docs/CLI_MANAGEMENT.md +5 -3
- package/docs/COMPOSITE_VISION.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +12 -2
- package/docs/DSH_CORE_MIGRATION.md +7 -3
- package/docs/FAULT_INJECTION.md +11 -1
- package/docs/HEADLESS_CLI.md +10 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +1 -1
- package/docs/HOST_DOCTOR.md +3 -3
- package/docs/MODEL_PORT_CONTRACT.md +9 -3
- package/docs/MUSIC.md +77 -0
- package/docs/OCR_MODEL.md +1 -1
- package/docs/PROMPT_OPTIMIZER.md +74 -6
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +12 -2
- package/docs/PROVIDER_HEALTH.md +1 -1
- package/docs/ROADMAP.md +15 -18
- package/docs/SECURITY.md +4 -3
- package/docs/SYNC_DELIVERY_RECOVERY.md +39 -0
- package/docs/TASK_SEMANTICS.md +5 -5
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +1 -1
- package/docs/WORKBENCH_ARTIFACTS.md +1 -1
- package/docs/releases/0.2.2.md +31 -0
- package/docs/releases/0.2.3.md +54 -0
- package/lib/actions.js +52 -19
- package/lib/adapters.js +89 -24
- package/lib/api.js +14 -5
- package/lib/bundled-skills.js +1 -1
- package/lib/capability.js +25 -16
- package/lib/client.js +374 -43
- package/lib/command-service.js +12 -3
- package/lib/config.js +39 -6
- package/lib/core-artifact-store.js +44 -6
- package/lib/core-delivery-cache.js +145 -0
- package/lib/core-lease-recovery.js +64 -92
- package/lib/core-maintenance.js +12 -1
- package/lib/core-runtime.js +45 -47
- package/lib/core-tasks.js +19 -5
- package/lib/core-user-projection.js +2 -2
- package/lib/core-writer-lease.js +134 -0
- package/lib/dsh-core-adapter.js +14 -4
- package/lib/dsh-text-model-adapter.js +15 -2
- package/lib/generation-input.js +10 -0
- package/lib/host-runtime.js +4 -2
- package/lib/index.js +23 -4
- package/lib/models.js +23 -38
- package/lib/music-generation.js +21 -0
- package/lib/music-options.js +18 -0
- package/lib/music-transport.js +46 -0
- package/lib/prompt-optimizer-config.js +111 -30
- package/lib/prompt-optimizer-core.js +183 -12
- package/lib/prompt-optimizer.js +20 -14
- package/lib/provider-adapter.js +1 -0
- package/lib/provider-adapters.js +26 -2
- package/lib/provider-catalog.js +18 -7
- package/lib/provider-config-service.js +16 -5
- package/lib/provider-protocol.js +73 -0
- package/lib/provider-task-runner.js +86 -9
- package/package.json +4 -2
- package/user_guide.md +52 -24
package/lib/models.js
CHANGED
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
* Iris 模型发现规则(阶段 6)。
|
|
4
4
|
*
|
|
5
5
|
* 目标:把「账号(key)」与「能力」解耦——一个 key 提供模型池,能力从池里选模型。
|
|
6
|
-
*
|
|
6
|
+
* 名称规则仅为建议;实际模型能力来自明确标签或可信发现元数据。
|
|
7
7
|
*
|
|
8
8
|
* 池的来源:
|
|
9
9
|
* ① provider 显式 `models` 数组(未来配置,可带 capabilities 覆盖)
|
|
10
10
|
* ② 旧模型字段迁移:imageModel→image-gen、videoModel→video-gen、ttsModel→tts、
|
|
11
11
|
* transcribeModel→transcribe、visionModel→vision
|
|
12
|
-
*
|
|
12
|
+
* 裸账号不物化厂商默认模型,也不参与运行时回退。
|
|
13
13
|
*/
|
|
14
|
-
import { CAPABILITIES } from './capability.js';
|
|
15
|
-
import { isDashScopeBaseUrl, selectMediaProtocol } from './provider-protocol.js';
|
|
14
|
+
import { CAPABILITIES, configuredModelCapabilities } from './capability.js';
|
|
16
15
|
import { modelRateLimit } from './provider-health.js';
|
|
17
16
|
|
|
18
17
|
/** 能力 → provider 上的模型字段(保持向后兼容的返回形状) */
|
|
@@ -20,6 +19,7 @@ export const CAP_FIELD = {
|
|
|
20
19
|
[CAPABILITIES.IMAGE]: 'imageModel',
|
|
21
20
|
[CAPABILITIES.VIDEO]: 'videoModel',
|
|
22
21
|
[CAPABILITIES.TTS]: 'ttsModel',
|
|
22
|
+
[CAPABILITIES.MUSIC]: 'musicModel',
|
|
23
23
|
[CAPABILITIES.TRANSCRIBE]: 'transcribeModel',
|
|
24
24
|
[CAPABILITIES.VISION]: 'visionModel'
|
|
25
25
|
};
|
|
@@ -64,29 +64,7 @@ const MODEL_CAP_RULES = [
|
|
|
64
64
|
[/^gemini/i, [CAPABILITIES.VISION]] // Gemini 默认按视觉能力纳入(可被显式配置覆盖)
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
const KNOWN_MODELS_BY_PROTOCOL = { dashscope: [
|
|
69
|
-
{ id: 'wan2.2-t2i-flash', caps: [CAPABILITIES.IMAGE] },
|
|
70
|
-
{ id: 'wan2.2-t2v-flash', caps: [CAPABILITIES.VIDEO] },
|
|
71
|
-
{ id: 'wan2.2-s2v-flash', caps: [CAPABILITIES.VIDEO] },
|
|
72
|
-
{ id: 'qwen-vl-plus', caps: [CAPABILITIES.VISION] },
|
|
73
|
-
// VERIFY 2026-09-03 实证:locate grounding 零偏差(qwen-vl-plus 仅 25x25 框选)
|
|
74
|
-
{ id: 'qwen3-vl-235b-a22b-thinking', caps: [CAPABILITIES.VISION] },
|
|
75
|
-
{ id: 'qwen-tts-latest', caps: [CAPABILITIES.TTS] },
|
|
76
|
-
{ id: 'qwen-audio-3.0-asr-flash-filetrans', caps: [CAPABILITIES.TRANSCRIBE] }
|
|
77
|
-
] };
|
|
78
|
-
|
|
79
|
-
export function migrateProviderModels(provider) {
|
|
80
|
-
if (Array.isArray(provider.models) || Object.values(CAP_FIELD).some((field) => provider[field])) return false;
|
|
81
|
-
const protocol = selectMediaProtocol(provider).mediaProtocol;
|
|
82
|
-
if (!isDashScopeBaseUrl(provider.baseUrl) || !Object.hasOwn(KNOWN_MODELS_BY_PROTOCOL, protocol)) return false;
|
|
83
|
-
provider.models = KNOWN_MODELS_BY_PROTOCOL[protocol].map((m) => ({
|
|
84
|
-
id: m.id, capabilities: [...m.caps], source: 'migration'
|
|
85
|
-
}));
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** 按模型名规则推断能力 */
|
|
67
|
+
/** 模型名建议,仅用于提示;不能直接授权选型或落盘能力。 */
|
|
90
68
|
export function capabilitiesOfModel(modelName) {
|
|
91
69
|
const name = String(modelName || '');
|
|
92
70
|
for (const [re, caps] of MODEL_CAP_RULES) {
|
|
@@ -100,17 +78,22 @@ const DISCOVERY_CAPABILITIES = Object.freeze({
|
|
|
100
78
|
IG: CAPABILITIES.IMAGE, IMAGE: CAPABILITIES.IMAGE, 'IMAGE-GEN': CAPABILITIES.IMAGE,
|
|
101
79
|
VG: CAPABILITIES.VIDEO, VIDEO: CAPABILITIES.VIDEO, 'VIDEO-GEN': CAPABILITIES.VIDEO,
|
|
102
80
|
TTS: CAPABILITIES.TTS, ASR: CAPABILITIES.TRANSCRIBE,
|
|
81
|
+
MUSIC: CAPABILITIES.MUSIC, 'MUSIC-GEN': CAPABILITIES.MUSIC,
|
|
103
82
|
TRANSCRIBE: CAPABILITIES.TRANSCRIBE, VU: CAPABILITIES.VISION, VISION: CAPABILITIES.VISION
|
|
104
83
|
});
|
|
105
84
|
|
|
106
|
-
/**
|
|
85
|
+
/** 只映射明确发现元数据;名字、描述和未知代码不证明能力。 */
|
|
107
86
|
export function capabilitiesOfDiscoveredModel(model) {
|
|
108
|
-
|
|
109
|
-
const declared = Array.isArray(model?.capabilities)
|
|
87
|
+
return Array.isArray(model?.capabilities)
|
|
110
88
|
? [...new Set(model.capabilities.map((value) => DISCOVERY_CAPABILITIES[String(value).toUpperCase()]).filter(Boolean))]
|
|
111
89
|
: [];
|
|
112
|
-
|
|
113
|
-
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 已存的明确配置优先于再发现;未标注名称仍可接收可信元数据。 */
|
|
93
|
+
export function hasConfiguredModelCapabilities(provider, modelId) {
|
|
94
|
+
const entry = provider.models?.find((model) => String(typeof model === 'string' ? model : model?.id || '').trim() === modelId);
|
|
95
|
+
if (Array.isArray(entry?.capabilities)) return entry.source !== 'discovered';
|
|
96
|
+
return configuredModelCapabilities(provider, { id: modelId }).length > 0;
|
|
114
97
|
}
|
|
115
98
|
|
|
116
99
|
/** 一个 provider 的模型池条目列表:[{ id, providerId, capabilities:[], verified?, source? }] */
|
|
@@ -125,23 +108,25 @@ export function providerModels(p) {
|
|
|
125
108
|
};
|
|
126
109
|
// ① 显式 models 数组(带可选 capabilities 覆盖 + verified/source 透传)
|
|
127
110
|
// 该字段一旦存在(含空数组)即为权威声明:用户可明确标记「此供应商没有可用模型」,
|
|
128
|
-
// 不得再落入 ②
|
|
111
|
+
// 不得再落入 ② 旧字段绑定。空数组曾因 `.length` 判定被穿透,
|
|
129
112
|
// 导致用户声明无模型却仍被注入 7 个厂商默认模型(见审计 B-5)。
|
|
130
113
|
if (Array.isArray(p.models)) {
|
|
131
114
|
for (const m of p.models) {
|
|
132
115
|
const id = typeof m === 'string' ? m : m && m.id;
|
|
133
|
-
//
|
|
134
|
-
const caps =
|
|
116
|
+
// 没有明确能力的名称保留为未标注模型,不进入候选链。
|
|
117
|
+
const caps = configuredModelCapabilities(p, m);
|
|
135
118
|
const extra = typeof m === 'object' && m ? { verified: m.verified, source: m.source,
|
|
136
119
|
...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
|
|
120
|
+
...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
|
|
137
121
|
...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}) } : {};
|
|
138
122
|
push(id, caps, extra);
|
|
139
123
|
}
|
|
140
124
|
return out;
|
|
141
125
|
}
|
|
142
126
|
// ② 旧模型字段迁移
|
|
143
|
-
for (const
|
|
144
|
-
|
|
127
|
+
for (const field of Object.values(CAP_FIELD)) {
|
|
128
|
+
const caps = configuredModelCapabilities(p, p[field]);
|
|
129
|
+
if (caps.length) push(p[field], caps);
|
|
145
130
|
}
|
|
146
131
|
return out;
|
|
147
132
|
}
|
|
@@ -185,7 +170,7 @@ export function orderedModels(pool, capability, assignments, { includeRateLimite
|
|
|
185
170
|
for (const raw of (Array.isArray(assignments) ? assignments : (assignments ? [assignments] : []))) {
|
|
186
171
|
add(resolvePoolModel(pool, raw, capability));
|
|
187
172
|
}
|
|
188
|
-
for (const model of pool) add(model);
|
|
173
|
+
if (capability !== CAPABILITIES.MUSIC) for (const model of pool) add(model);
|
|
189
174
|
return ordered;
|
|
190
175
|
}
|
|
191
176
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
|
|
3
|
+
/** Explicit Music 502 regeneration policy; each resubmission is a new Core Task. */
|
|
4
|
+
export async function generateMusicTask({ runner, candidates, providerInput, options, signal }) {
|
|
5
|
+
const taskIds = [];
|
|
6
|
+
let result, retriedFrom;
|
|
7
|
+
for (let index = 0; index <= options.retry502; index++) {
|
|
8
|
+
signal?.throwIfAborted();
|
|
9
|
+
result = await runner.submit({ capability: 'music', candidates, providerInput, ...(retriedFrom ? { retriedFrom } : {}) });
|
|
10
|
+
taskIds.push(result.task.id);
|
|
11
|
+
const task = result.task;
|
|
12
|
+
if (result.localError || task.phase !== 'terminal' || task.lastError?.httpStatus !== 502
|
|
13
|
+
|| task.outcome === 'succeeded' || index === options.retry502) break;
|
|
14
|
+
// Retry the actual model, without switching to another (potentially paid) source.
|
|
15
|
+
candidates = candidates.filter(candidate => candidate.model === task.modelRef && candidate.adapter.id === task.providerId);
|
|
16
|
+
if (!candidates.length) throw new Error('Music 重试无法找到原模型绑定');
|
|
17
|
+
retriedFrom = task.id;
|
|
18
|
+
await delay(options.retryDelayMs, undefined, { signal });
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({ ...result, taskIds: Object.freeze(taskIds), retries: taskIds.length - 1 });
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Music caller controls; generation parameters remain in the prompt. */
|
|
2
|
+
export function normalizeMusicOptions(input = {}) {
|
|
3
|
+
const integer = (field, fallback, min, max) => {
|
|
4
|
+
if (input[field] !== undefined && !['number', 'string'].includes(typeof input[field])) {
|
|
5
|
+
throw new TypeError(`${field} 必须是整数`);
|
|
6
|
+
}
|
|
7
|
+
const value = input[field] === undefined ? fallback : Number(input[field]);
|
|
8
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
9
|
+
throw new TypeError(`${field} 必须是 ${min}–${max} 的整数`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
timeoutMs: integer('timeout_ms', 300000, 1, 3600000),
|
|
15
|
+
retry502: integer('retry_502', 2, 0, 5),
|
|
16
|
+
retryDelayMs: integer('retry_delay_ms', 5000, 0, 60000)
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ProviderContractError } from './provider-contract.js';
|
|
2
|
+
|
|
3
|
+
function responseError(message) {
|
|
4
|
+
return new ProviderContractError(message, { stage: 'response', category: 'protocol', acceptance: 'unknown' });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Gemini web gateway's documented non-streaming Markdown audio response. */
|
|
8
|
+
export function musicArtifactsFromResponse(body) {
|
|
9
|
+
if (!Array.isArray(body?.choices)) throw responseError('Music 响应缺少 choices');
|
|
10
|
+
const artifacts = [], seen = new Set();
|
|
11
|
+
for (const choice of body.choices) {
|
|
12
|
+
const content = choice?.message?.content;
|
|
13
|
+
if (typeof content !== 'string') continue;
|
|
14
|
+
for (const match of content.matchAll(/data:(audio\/(?:mpeg|wav|x-wav));base64,([A-Za-z0-9+/=]+)/gi)) {
|
|
15
|
+
const data = match[2];
|
|
16
|
+
const bytes = Buffer.from(data, 'base64');
|
|
17
|
+
if (!bytes.length || bytes.toString('base64') !== data) throw responseError('Music 音频 base64 无效');
|
|
18
|
+
if (seen.has(data)) continue;
|
|
19
|
+
seen.add(data);
|
|
20
|
+
artifacts.push({ kind: 'inline-base64', data, mediaType: match[1].toLowerCase() === 'audio/mpeg' ? 'audio/mpeg' : 'audio/wav' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!artifacts.length) throw responseError('Music 响应没有可交付的 MP3/WAV 音频');
|
|
24
|
+
return artifacts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function generateMusic({ key, baseUrl, model, prompt, timeoutMs = 300000, signal }) {
|
|
28
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
29
|
+
const response = await fetch(String(baseUrl).replace(/\/+$/, '') + '/chat/completions', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json', ...(key ? { Authorization: `Bearer ${key}` } : {}) },
|
|
32
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
33
|
+
body: JSON.stringify({ model, stream: false, messages: [{ role: 'user', content: prompt }] })
|
|
34
|
+
});
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
let body;
|
|
37
|
+
try { body = JSON.parse(text); }
|
|
38
|
+
catch (error) { if (response.ok) throw responseError('Music 响应不是有效 JSON'); }
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const message = String(body?.error?.message || body?.message || `Music HTTP ${response.status}`);
|
|
41
|
+
throw new ProviderContractError(key ? message.replaceAll(key, '[REDACTED]') : message, {
|
|
42
|
+
stage: 'submit', httpStatus: response.status, category: 'provider', acceptance: 'unknown', retryable: response.status === 502
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return musicArtifactsFromResponse(body);
|
|
46
|
+
}
|
|
@@ -17,16 +17,26 @@ const DEFAULT_VALUE = {
|
|
|
17
17
|
version: 1,
|
|
18
18
|
enabled: true,
|
|
19
19
|
systemPrompt: [
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
|
|
20
|
+
'# Iris 提示词编辑器',
|
|
21
|
+
'## 职责',
|
|
22
|
+
'按用户选择的功能,把草稿整理为可直接交给目标 AI 的提示词。只编辑文字,不执行工具或媒体生成。',
|
|
23
|
+
'## 保留用户意图',
|
|
24
|
+
'保留原始目标、事实、专有名词、引号内原文、硬性约束和否定条件。尤其保留主体身份与数量、左右方位、颜色、数值与单位、构图、时序和明确指定的风格,不擅自替换或扩展。',
|
|
25
|
+
'逐项保留用户已写出的具体描述,不把这些细节当成冗余删去;明确否定条件不得用更宽泛的词概括、替换或扩大。没有依据时,不增加可被观察到的新属性、位置、声音、动作或光照。',
|
|
26
|
+
'用户明示的事实和硬性约束优先于通用模板建议;只有用户本次改写规则明确授权的具体内容可以改变,其余细节与约束继续保留。信息不足或存在冲突时保持未指定或待确认,不替用户作关键决定。',
|
|
27
|
+
'素材引用只是引用。没有看到图片、听到音频或读到文件时,不声称已分析其内容;不编造 Artifact ID、附件 ID、路径、模型、工具参数或执行结果。',
|
|
28
|
+
'## 组织方式',
|
|
29
|
+
'参照 Iris Skills 的任务组织:明确目标与必要输入,写清具体操作、必须保持和允许改变的部分,以及用户要求的输出和验收条件。只使用与本次功能有关的维度,不堆砌质量词或无依据的细节。',
|
|
30
|
+
'默认沿用草稿语言;只允许分段、标点和明确等义的措辞。缺失维度无需补齐;不把几个改为三五位,不增加低声聊天、背景模糊或清晰、书本位置等内容,不把人物左右推为画面左右。已有内容完整时允许不改写。条件、否定、指代与动作顺序不能拆散或擅自重排。',
|
|
31
|
+
'## 输出',
|
|
32
|
+
'遵守本次编辑策略指定的输出协议;普通改写只输出完整提示词正文,不解释、不加额外标题、不使用 Markdown 代码块或工具调用 JSON。补充建议不混入正文,未经用户采纳不视为用户要求。'
|
|
33
|
+
].join('\n\n'),
|
|
25
34
|
targets: {
|
|
26
|
-
general: '
|
|
27
|
-
image: '
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
general: '# 通用任务\n\n## 目标\n把用户任务整理为明确、可执行的指令。\n\n## 组织\n按需要写清目标、必要背景与输入、约束、期望输出和验收标准;不添加用户未要求的工作步骤或背景事实。',
|
|
36
|
+
image: '# 画图 · iris_draw_image\n\n## 目标\n整理已有的画图提示词文字,不执行画图。\n\n## 组织\n按已提供的主体与动作、环境、构图与视角、光影、色彩、材质和风格分组。分类维度不是待补全的要求,未指定内容继续未指定。不添加人物、物体、画风、画幅或新关系。\n\n## 保留\n准确保留数量、约数、年龄、左右方位、服装、颜色、程度、姿态及否定范围;仅用户明确授权改变的项目可以改变。文字里的尺寸要求仍是用户要求,不冒充工具参数已设置。',
|
|
37
|
+
'image-edit': '# 改图 · iris_edit_image\n\n## 目标\n以已有原图为基础,只修改用户明确指定的部分。\n\n## 组织\n写清需要改变的对象、属性或区域,以及必须保持的主体身份、构图、背景、风格和其余细节。修改范围越小,指令越聚焦。\n\n## 输入与约束\n原图未提供可见内容时,不猜测原图颜色、位置或人物细节;用户提供的来源 ID 保持为引用。不要把改图改写成重新画一张,也不擅自增加重绘、遮罩或多参考图工作流。',
|
|
38
|
+
video: '# 视频 · iris_generate_video\n\n## 目标\n描述用户要生成的文生或图生视频。\n\n## 组织\n围绕主体动作、动作先后、场景变化、镜头运动与景别、节奏和连续性组织可拍摄的过程。保留用户给出的时长、构图与负面约束,不擅自增加分镜、声音或时长参数。\n\n## 输入与约束\n已有首帧或参考图时,只依据用户明确提供的内容保持主体与空间关系;不虚构已经查看素材。不要用静态画质词替代动作和时间描述。',
|
|
39
|
+
s2v: '# 数字人 · iris_generate_video(S2V)\n\n## 输入\n此功能依据首帧图像与音频生成视频;S2V 不是首尾帧插值。\n\n## 目标与组织\n在既定人物和已有音频约束下,描述与用户要求一致的表情、口型、姿态、动作和镜头。保持身份、服装、背景及动作连续性,不擅自改写音频台词、添加声音或替换人物。\n\n## 约束\n音频内容或图像不可见时,不猜测台词、时长、情绪或外观细节;已有素材无需再次生成。'
|
|
30
40
|
},
|
|
31
41
|
route: {
|
|
32
42
|
mode: 'session'
|
|
@@ -108,62 +118,133 @@ export function normalizePromptOptimizerConfig(input) {
|
|
|
108
118
|
};
|
|
109
119
|
}
|
|
110
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
|
+
|
|
111
136
|
let cache = null;
|
|
112
137
|
let cacheSource = 'default';
|
|
138
|
+
let cacheFeedback = { warnings: [] };
|
|
113
139
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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;
|
|
121
153
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function useFallback(file, reason, isolate) {
|
|
157
|
+
let isolation = 'not-attempted';
|
|
158
|
+
if (isolate) {
|
|
126
159
|
const backup = privateSibling(file, 'corrupted');
|
|
127
160
|
try {
|
|
128
161
|
fs.renameSync(file, backup);
|
|
162
|
+
isolation = 'saved';
|
|
129
163
|
chmodPrivateFile(backup);
|
|
130
|
-
} catch (_) {
|
|
131
|
-
|
|
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);
|
|
132
181
|
cache = structuredClone(DEFAULT_VALUE);
|
|
133
182
|
cacheSource = 'default';
|
|
183
|
+
cacheFeedback = { warnings: [] };
|
|
184
|
+
return snapshot();
|
|
134
185
|
}
|
|
135
|
-
|
|
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();
|
|
136
195
|
}
|
|
137
196
|
|
|
138
197
|
export function importPromptOptimizerConfig(input) {
|
|
139
198
|
const next = normalizePromptOptimizerConfig(input);
|
|
140
|
-
|
|
199
|
+
persistConfig(next);
|
|
141
200
|
cache = next;
|
|
142
201
|
cacheSource = 'custom';
|
|
143
|
-
|
|
202
|
+
cacheFeedback = importFeedback(input);
|
|
203
|
+
return snapshot();
|
|
144
204
|
}
|
|
145
205
|
|
|
146
206
|
export function setPromptOptimizerEnabled(enabled) {
|
|
147
207
|
if (typeof enabled !== 'boolean') throw new Error('enabled 必须是布尔值');
|
|
148
|
-
const
|
|
149
|
-
|
|
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);
|
|
150
228
|
cache = next;
|
|
151
229
|
cacheSource = JSON.stringify(cache) === JSON.stringify(DEFAULT_VALUE) ? 'default' : 'custom';
|
|
152
|
-
|
|
230
|
+
cacheFeedback = { warnings: [] };
|
|
231
|
+
return snapshot();
|
|
153
232
|
}
|
|
154
233
|
|
|
155
234
|
export function resetPromptOptimizerConfig() {
|
|
156
235
|
const next = structuredClone(DEFAULT_VALUE);
|
|
157
|
-
|
|
236
|
+
persistConfig(next);
|
|
158
237
|
cache = next;
|
|
159
238
|
cacheSource = 'default';
|
|
160
|
-
|
|
239
|
+
cacheFeedback = { warnings: [] };
|
|
240
|
+
return snapshot();
|
|
161
241
|
}
|
|
162
242
|
|
|
163
243
|
/** 仅供测试/热重载丢弃进程内缓存。 */
|
|
164
244
|
export function resetPromptOptimizerConfigCache() {
|
|
165
245
|
cache = null;
|
|
166
246
|
cacheSource = 'default';
|
|
247
|
+
cacheFeedback = { warnings: [] };
|
|
167
248
|
}
|
|
168
249
|
|
|
169
250
|
export function promptOptimizerConfigFile() {
|