@mokuyoaxis/dsh-iris 0.2.5 → 0.2.7
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-use/SKILL.md +2 -2
- package/.dsh/skills/iris-use/references/media-contract.md +2 -2
- package/CHANGELOG.md +49 -1
- package/README.en.md +39 -22
- package/README.md +40 -21
- package/bin/dsh-iris.js +166 -35
- package/docs/AGENT_QUICKSTART.md +7 -7
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CLI_MANAGEMENT.md +5 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
- package/docs/CORE_SKILLS.md +62 -0
- package/docs/DSH_COMPATIBILITY.md +8 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
- package/docs/DSH_RUNTIME.md +36 -0
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
- package/docs/FUNCTION_MODEL_POOLS.md +84 -0
- package/docs/HEADLESS_CLI.md +16 -8
- package/docs/HOST_ADAPTER_CONTRACT.md +7 -2
- package/docs/HOST_DOCTOR.md +14 -2
- package/docs/IMAGE_FLOW.md +71 -0
- package/docs/LOCAL_API.md +60 -0
- package/docs/LOCAL_API_CONTRACT.md +115 -0
- package/docs/LOCAL_CREATION.md +50 -0
- package/docs/MUSIC.md +2 -2
- package/docs/PROVIDER_HEALTH.md +26 -2
- package/docs/ROADMAP.md +18 -4
- package/docs/SECURITY.md +20 -2
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +47 -0
- package/docs/releases/0.2.6.md +50 -0
- package/docs/releases/0.2.7.md +62 -0
- package/lib/actions.js +208 -90
- package/lib/adapters.js +36 -9
- package/lib/api.js +40 -39
- package/lib/application-runtime.js +186 -0
- package/lib/client.js +805 -182
- package/lib/composite-vision-routing.js +6 -6
- package/lib/config.js +140 -29
- package/lib/core-artifact-store.js +5 -0
- package/lib/core-maintenance.js +25 -2
- package/lib/core-runtime.js +19 -0
- package/lib/core-tasks.js +62 -8
- package/lib/creation-request-store.js +159 -0
- package/lib/doctor.js +20 -2
- package/lib/dsh-core-adapter.js +669 -564
- package/lib/dsh-host-adapter.js +14 -1
- package/lib/dsh-provider-config.js +35 -0
- package/lib/dsh-provider-source.js +102 -0
- package/lib/flow-store.js +195 -0
- package/lib/headless-vision.js +5 -5
- package/lib/host-contract.js +1 -0
- package/lib/http-vision-model-adapter.js +5 -2
- package/lib/image-flow.js +188 -0
- package/lib/index.js +103 -78
- package/lib/local-api-projection.js +27 -0
- package/lib/local-api.js +381 -0
- package/lib/local-creation.js +85 -0
- package/lib/model-directory-service.js +53 -0
- package/lib/model-directory.js +103 -0
- package/lib/models.js +71 -6
- package/lib/ocr-model-routing.js +2 -2
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapters.js +42 -9
- package/lib/provider-catalog.js +14 -9
- package/lib/provider-config-scope.js +16 -0
- package/lib/provider-config-service.js +82 -13
- package/lib/provider-health.js +33 -0
- package/lib/provider-protocol.js +30 -0
- package/lib/provider-task-runner.js +22 -2
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +199 -0
- package/lib/vision-model-routing.js +25 -12
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-management.js +8 -9
- package/package.json +4 -3
- package/skills/iris-core/SKILL.md +40 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/tasks.md +58 -0
- package/user_guide.md +32 -20
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Private local creation intents. No Host, HTTP, Provider or Runtime dependencies.
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { atomicCreatePrivate, atomicWritePrivate } from './private-storage.js';
|
|
6
|
+
import { inspectFlowRoot, validateFlowCreation } from './flow-store.js';
|
|
7
|
+
import { WRITER_LEASE_DIR, WRITER_INTENT_FILE } from './core-writer-lease.js';
|
|
8
|
+
|
|
9
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
10
|
+
const ID = /^req_[a-f0-9]{64}$/;
|
|
11
|
+
const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
12
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
13
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
14
|
+
const TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
15
|
+
const MAX_IMAGE = 20 * 1024 * 1024;
|
|
16
|
+
const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
17
|
+
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
18
|
+
const copy = value => structuredClone(value);
|
|
19
|
+
function keys(value, allowed, required = allowed) {
|
|
20
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
+
&& Object.keys(value).every(key => allowed.includes(key)) && required.every(key => Object.hasOwn(value, key));
|
|
22
|
+
}
|
|
23
|
+
export function creationFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
24
|
+
const invalid = () => creationFail('IRIS_CREATION_STORE_INVALID', '创建请求区未知、损坏或尚未完整初始化');
|
|
25
|
+
function directory(root, segments, create = false) {
|
|
26
|
+
let current = root;
|
|
27
|
+
for (const segment of segments) {
|
|
28
|
+
current = path.join(current, segment);
|
|
29
|
+
if (create && !fs.existsSync(current)) fs.mkdirSync(current, { mode: 0o700 });
|
|
30
|
+
const stat = fs.lstatSync(current);
|
|
31
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
function jsonFile(file) {
|
|
36
|
+
const stat = fs.lstatSync(file);
|
|
37
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) invalid();
|
|
38
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { invalid(); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function inspectCreationRoot(root, { allowAbsent = false } = {}) {
|
|
42
|
+
let base;
|
|
43
|
+
try { base = directory(root, ['creation-requests']); }
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === 'ENOENT' && !fs.existsSync(path.join(root, 'creation-requests'))) {
|
|
46
|
+
if (allowAbsent) return null;
|
|
47
|
+
creationFail('IRIS_CREATION_ROOT_REQUIRED', '请先在空的专用 Flow 根显式执行 api init');
|
|
48
|
+
}
|
|
49
|
+
if (error.code === 'ENOENT') invalid();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (fs.readdirSync(base).some(name => name !== 'v0')) invalid();
|
|
54
|
+
const version = directory(root, ['creation-requests', 'v0']);
|
|
55
|
+
if (fs.readdirSync(version).some(name => !['root.json', 'requests'].includes(name)
|
|
56
|
+
&& !/^root\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name))) invalid();
|
|
57
|
+
const marker = jsonFile(path.join(version, 'root.json'));
|
|
58
|
+
if (!keys(marker, ['schemaVersion', 'kind', 'rootId', 'createdAt']) || marker.schemaVersion !== 0
|
|
59
|
+
|| marker.kind !== 'iris-creation-requests' || !UUID.test(marker.rootId) || !iso(marker.createdAt)) invalid();
|
|
60
|
+
directory(root, ['creation-requests', 'v0', 'requests']);
|
|
61
|
+
return copy(marker);
|
|
62
|
+
} catch (error) { if (error.code === 'ENOENT') invalid(); throw error; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function initializeCreationRoot(root) {
|
|
66
|
+
const existing = inspectCreationRoot(root, { allowAbsent: true });
|
|
67
|
+
if (existing) { listCreationRequests(root); return existing; }
|
|
68
|
+
inspectFlowRoot(root);
|
|
69
|
+
if (fs.readdirSync(root).some(name => !['flow-store', WRITER_LEASE_DIR, WRITER_INTENT_FILE].includes(name))
|
|
70
|
+
|| fs.readdirSync(path.join(root, 'flow-store')).some(name => name !== 'v0')
|
|
71
|
+
|| fs.readdirSync(path.join(root, 'flow-store/v0')).some(name => !['root.json', 'runs'].includes(name))
|
|
72
|
+
|| fs.readdirSync(path.join(root, 'flow-store/v0/runs')).length) {
|
|
73
|
+
creationFail('IRIS_CREATION_ROOT_NOT_EMPTY', '请求区仅在新的空 Flow 根初始化;不转换已有创作数据');
|
|
74
|
+
}
|
|
75
|
+
const marker = { schemaVersion: 0, kind: 'iris-creation-requests', rootId: crypto.randomUUID(), createdAt: new Date().toISOString() };
|
|
76
|
+
directory(root, ['creation-requests', 'v0', 'requests'], true);
|
|
77
|
+
atomicCreatePrivate(path.join(root, 'creation-requests/v0/root.json'), JSON.stringify(marker, null, 2) + '\n');
|
|
78
|
+
return copy(marker);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function creationRequestId(marker, kind, key) {
|
|
82
|
+
if (!UUID.test(key)) creationFail('IRIS_CREATION_KEY_INVALID', 'Idempotency-Key 须为单个小写 UUIDv4');
|
|
83
|
+
if (!['image-import', 'flow-create'].includes(kind)) throw new TypeError('未知本地创建类型');
|
|
84
|
+
return 'req_' + hash(JSON.stringify(['creation-requests/v0', marker.rootId, kind, key]));
|
|
85
|
+
}
|
|
86
|
+
export function creationInputDigest(kind, input) {
|
|
87
|
+
return 'sha256:' + hash(JSON.stringify(kind === 'image-import'
|
|
88
|
+
? ['image-import/v0', input.mediaType, input.size, input.sha256]
|
|
89
|
+
: ['flow-create/v0', 'image-edit/v0', input.source_artifact_id, input.model_ref, input.prompt, input.size ?? null, 1]));
|
|
90
|
+
}
|
|
91
|
+
function validate(record, id, marker) {
|
|
92
|
+
if (!keys(record, ['schemaVersion', 'id', 'rootId', 'kind', 'inputDigest', 'createdAt', 'intent', 'state', 'completedAt', 'result'],
|
|
93
|
+
['schemaVersion', 'id', 'rootId', 'kind', 'inputDigest', 'createdAt', 'intent', 'state'])
|
|
94
|
+
|| record.schemaVersion !== 0 || !ID.test(id) || record.id !== id || record.rootId !== marker.rootId
|
|
95
|
+
|| !DIGEST.test(record.inputDigest) || !iso(record.createdAt) || record.intent?.createdAt !== record.createdAt
|
|
96
|
+
|| !['prepared', 'succeeded'].includes(record.state)) invalid();
|
|
97
|
+
const intent = record.intent;
|
|
98
|
+
let expectedResult, expectedDigest;
|
|
99
|
+
if (record.kind === 'image-import') {
|
|
100
|
+
if (!keys(intent, ['artifactId', 'mediaType', 'size', 'sha256', 'metadata', 'createdAt'])
|
|
101
|
+
|| !ARTIFACT.test(intent.artifactId) || !TYPES.has(intent.mediaType) || !SHA.test(intent.sha256)
|
|
102
|
+
|| !Number.isSafeInteger(intent.size) || intent.size < 1 || intent.size > MAX_IMAGE
|
|
103
|
+
|| !keys(intent.metadata, ['width', 'height']) || !['width', 'height'].every(key => Number.isSafeInteger(intent.metadata[key]) && intent.metadata[key] > 0)) invalid();
|
|
104
|
+
expectedDigest = creationInputDigest(record.kind, intent);
|
|
105
|
+
expectedResult = { artifactId: intent.artifactId };
|
|
106
|
+
} else if (record.kind === 'flow-create') {
|
|
107
|
+
try { validateFlowCreation(intent); } catch (_) { invalid(); }
|
|
108
|
+
expectedDigest = creationInputDigest(record.kind, intent.input);
|
|
109
|
+
expectedResult = { runId: intent.runId };
|
|
110
|
+
} else invalid();
|
|
111
|
+
if (record.inputDigest !== expectedDigest) invalid();
|
|
112
|
+
if (record.state === 'prepared') {
|
|
113
|
+
if (Object.hasOwn(record, 'completedAt') || Object.hasOwn(record, 'result')) invalid();
|
|
114
|
+
} else if (!iso(record.completedAt) || !keys(record.result, Object.keys(expectedResult))
|
|
115
|
+
|| JSON.stringify(record.result) !== JSON.stringify(expectedResult)) invalid();
|
|
116
|
+
return record;
|
|
117
|
+
}
|
|
118
|
+
export function readCreationRequest(root, id, marker = inspectCreationRoot(root)) {
|
|
119
|
+
if (!ID.test(id)) invalid();
|
|
120
|
+
let record;
|
|
121
|
+
try { record = jsonFile(path.join(root, 'creation-requests/v0/requests', id + '.json')); }
|
|
122
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
123
|
+
return copy(validate(record, id, marker));
|
|
124
|
+
}
|
|
125
|
+
export function listCreationRequests(root) {
|
|
126
|
+
const marker = inspectCreationRoot(root, { allowAbsent: true });
|
|
127
|
+
if (!marker) return [];
|
|
128
|
+
const records = [];
|
|
129
|
+
for (const name of fs.readdirSync(path.join(root, 'creation-requests/v0/requests'))) {
|
|
130
|
+
if (/^req_[a-f0-9]{64}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name)) continue;
|
|
131
|
+
if (!/^req_[a-f0-9]{64}\.json$/.test(name)) invalid();
|
|
132
|
+
const record = readCreationRequest(root, name.slice(0, -5), marker);
|
|
133
|
+
if (!record) invalid();
|
|
134
|
+
records.push(record);
|
|
135
|
+
}
|
|
136
|
+
return records;
|
|
137
|
+
}
|
|
138
|
+
export function publishCreationRequest(root, record) {
|
|
139
|
+
const marker = inspectCreationRoot(root);
|
|
140
|
+
validate(record, record.id, marker);
|
|
141
|
+
atomicCreatePrivate(path.join(root, 'creation-requests/v0/requests', record.id + '.json'), JSON.stringify(record, null, 2) + '\n');
|
|
142
|
+
return copy(record);
|
|
143
|
+
}
|
|
144
|
+
export function completeCreationRequest(root, record) {
|
|
145
|
+
const current = readCreationRequest(root, record.id);
|
|
146
|
+
if (!current || JSON.stringify(current) !== JSON.stringify(record) || current.state !== 'prepared') invalid();
|
|
147
|
+
const finished = { ...record, state: 'succeeded', completedAt: new Date().toISOString(),
|
|
148
|
+
result: record.kind === 'image-import' ? { artifactId: record.intent.artifactId } : { runId: record.intent.runId } };
|
|
149
|
+
validate(finished, record.id, inspectCreationRoot(root));
|
|
150
|
+
atomicWritePrivate(path.join(root, 'creation-requests/v0/requests', record.id + '.json'), JSON.stringify(finished, null, 2) + '\n');
|
|
151
|
+
return copy(finished);
|
|
152
|
+
}
|
|
153
|
+
export function creationReferences(root) {
|
|
154
|
+
return listCreationRequests(root).filter(record => record.state === 'prepared').map(record => ({
|
|
155
|
+
requestId: record.id, taskIds: record.kind === 'flow-create' ? [record.intent.taskId] : [],
|
|
156
|
+
artifactIds: record.kind === 'image-import' ? [record.intent.artifactId]
|
|
157
|
+
: [record.intent.source.artifactId, record.intent.comparisonArtifactId]
|
|
158
|
+
}));
|
|
159
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -103,16 +103,23 @@ function inspectConfig(dataDir, checks) {
|
|
|
103
103
|
let incomplete = 0;
|
|
104
104
|
let enabled = 0;
|
|
105
105
|
let models = 0;
|
|
106
|
+
let hostReferences = 0;
|
|
106
107
|
for (const provider of providers) {
|
|
107
108
|
if (!plain(provider)) { incomplete++; continue; }
|
|
108
109
|
if (provider.enabled !== false) enabled++;
|
|
109
|
-
|
|
110
|
+
const reference = provider.dshProvider;
|
|
111
|
+
const managed = plain(reference) && typeof reference.provider === 'string' && reference.provider.trim()
|
|
112
|
+
&& typeof reference.settingsNs === 'string' && reference.settingsNs.trim();
|
|
113
|
+
if (provider.enabled !== false && managed) hostReferences++;
|
|
114
|
+
if (provider.enabled !== false && !managed && (!providerMediaBaseUrl(provider)
|
|
110
115
|
|| (provider.auth !== 'none' && !providerApiKey(provider).trim()))) incomplete++;
|
|
111
116
|
models += Array.isArray(provider.models) ? provider.models.length : 0;
|
|
112
117
|
}
|
|
113
118
|
if (incomplete) checks.push(result('providers', 'warn', `${incomplete} 个启用供应商缺少端点、所需凭据或结构无效(auth:none 不要求密钥)`, '在 Iris 工作台补全或停用对应供应商。'));
|
|
114
119
|
else if (!enabled) checks.push(result('providers', 'warn', '没有启用的供应商', '在 Iris 工作台启用至少一个供应商。'));
|
|
115
120
|
else checks.push(result('providers', 'ok', `${enabled} 个供应商已启用;凭据未输出`));
|
|
121
|
+
if (hostReferences) checks.push(result('provider-references', 'warn', `${hostReferences} 个 DSH 连接引用需由运行中宿主解析;离线未验证连接`,
|
|
122
|
+
'在 DSH 的 Iris 工作台检查原连接与功能池候选,无需在 Iris 重填宿主密钥。'));
|
|
116
123
|
const assignments = config.value.assignments || {};
|
|
117
124
|
const assignmentCount = Object.keys(assignments).length;
|
|
118
125
|
if (!models) checks.push(result('models', 'warn', '模型池为空或尚未发现模型', '在工作台对供应商执行“发现模型”,再核对能力标签。'));
|
|
@@ -414,7 +421,18 @@ export function hostDoctor(host, evidence = {}, options = {}) {
|
|
|
414
421
|
+ (missingSkills.length ? ':' + missingSkills.join(', ') : ''),
|
|
415
422
|
'工具仍可直接调用;检查 DSH Skill registry 和插件启动日志。'));
|
|
416
423
|
|
|
417
|
-
capabilityCheck(checks, snapshot, 'browser', 'browser', 'HTML Browser')
|
|
424
|
+
if (capabilityCheck(checks, snapshot, 'browser', 'browser', 'HTML Browser')) {
|
|
425
|
+
checks[checks.length - 1] = result('browser', 'ok', 'HTML 浏览器接口已提供;实际截图需单独自检',
|
|
426
|
+
'点击“截图自检”验证当前浏览器能否启动、打开页面并生成图片。');
|
|
427
|
+
const address = host.ports.browser.address;
|
|
428
|
+
if (address) {
|
|
429
|
+
checks.push(result('render-address', address.status === 'resolved' ? 'ok' : 'warn',
|
|
430
|
+
address.status === 'resolved'
|
|
431
|
+
? '截图地址:' + address.baseUrl + (address.source === 'environment' ? '(DSH_WEB_BASE)' : '(宿主监听)')
|
|
432
|
+
: address.message,
|
|
433
|
+
address.status === 'resolved' ? '' : '显式配置 DSH_WEB_BASE;不需要额外的端口转发。'));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
418
436
|
capabilityCheck(checks, snapshot, 'attachments', 'attachments', '附件读写');
|
|
419
437
|
capabilityCheck(checks, snapshot, 'sessions', 'sessions', '会话查询');
|
|
420
438
|
capabilityCheck(checks, snapshot, 'text-model', 'textModel', '宿主文本模型');
|