@mokuyoaxis/dsh-iris 0.2.6 → 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/CHANGELOG.md +23 -1
- package/README.en.md +28 -19
- package/README.md +27 -18
- package/bin/dsh-iris.js +140 -26
- package/docs/AGENT_QUICKSTART.md +5 -5
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
- package/docs/CORE_SKILLS.md +62 -0
- package/docs/DSH_COMPATIBILITY.md +4 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RUNTIME.md +36 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +11 -3
- package/docs/HOST_DOCTOR.md +13 -1
- 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/PROVIDER_HEALTH.md +1 -1
- package/docs/ROADMAP.md +14 -2
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/WEB_PROTOTYPE.md +47 -0
- package/docs/releases/0.2.7.md +62 -0
- package/lib/actions.js +79 -68
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +186 -0
- package/lib/client.js +89 -3
- 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 +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/flow-store.js +195 -0
- package/lib/image-flow.js +188 -0
- package/lib/index.js +59 -53
- package/lib/local-api-projection.js +27 -0
- package/lib/local-api.js +381 -0
- package/lib/local-creation.js +85 -0
- package/lib/private-storage.js +12 -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/workbench-artifacts.js +1 -2
- package/lib/workbench-management.js +8 -9
- package/package.json +3 -2
- 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 +14 -8
package/lib/core-tasks.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { atomicWritePrivate, ensurePrivateDir } from './private-storage.js';
|
|
6
|
+
import { atomicCreatePrivate, atomicWritePrivate, ensurePrivateDir } from './private-storage.js';
|
|
7
7
|
import { parseModelRef } from './models.js';
|
|
8
8
|
import { providerErrorRecord } from './provider-contract.js';
|
|
9
9
|
import { deriveLegacyStatus, semanticViolations } from './task-semantics.js';
|
|
10
10
|
import { readDeliveryCache } from './core-delivery-cache.js';
|
|
11
|
+
import { flowTaskBinding, readFlowRun, validFlowBinding } from './flow-store.js';
|
|
11
12
|
|
|
12
13
|
const STORE_DIR = 'task-store';
|
|
13
14
|
const STORE_VERSION = 'v0';
|
|
@@ -110,6 +111,7 @@ function validateRecord(record, expectedId) {
|
|
|
110
111
|
|| (record.deliveryCache !== undefined && record.deliveryCache !== true)
|
|
111
112
|
|| (record.providerBinding !== undefined && !PROVIDER_BINDING.test(record.providerBinding))
|
|
112
113
|
|| (record.retriedFrom !== undefined && !TASK_ID.test(record.retriedFrom))
|
|
114
|
+
|| (record.flowBinding !== undefined && (record.capability !== 'image' || !validFlowBinding(record.flowBinding)))
|
|
113
115
|
|| (record.sourceArtifactId !== undefined && (record.capability !== 'image' || !ARTIFACT_ID.test(record.sourceArtifactId)))) {
|
|
114
116
|
fail('IRIS_TASK_INVALID', 'Task 记录结构无效');
|
|
115
117
|
}
|
|
@@ -143,11 +145,12 @@ function readRecord(dataRoot, id) {
|
|
|
143
145
|
return validateRecord(record, id);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
function writeRecord(dataRoot, record) {
|
|
148
|
+
function writeRecord(dataRoot, record, exclusive = false) {
|
|
147
149
|
validateRecord(record, record.id);
|
|
148
150
|
try {
|
|
149
151
|
ensurePrivateDir(taskDirectory(dataRoot));
|
|
150
|
-
|
|
152
|
+
const publish = exclusive ? atomicCreatePrivate : atomicWritePrivate;
|
|
153
|
+
publish(taskFile(dataRoot, record.id), JSON.stringify(record, null, 2) + '\n');
|
|
151
154
|
} catch (error) {
|
|
152
155
|
if (error instanceof CoreTaskError) throw error;
|
|
153
156
|
fail('IRIS_TASK_WRITE_FAILED', 'Task 无法安全写入');
|
|
@@ -169,7 +172,7 @@ function safeError(error, overrides) {
|
|
|
169
172
|
return providerErrorRecord(error, overrides);
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
function newTaskRecord(input, id) {
|
|
173
176
|
const capability = String(input.capability || '').trim();
|
|
174
177
|
if (!CAPABILITIES.has(capability)) fail('IRIS_TASK_INPUT_INVALID', 'Task capability 无效');
|
|
175
178
|
const retriedFrom = input.retriedFrom === undefined ? undefined : String(input.retriedFrom);
|
|
@@ -177,10 +180,10 @@ export function createCoreTask(dataRoot, input = {}) {
|
|
|
177
180
|
const sourceArtifactId = input.sourceArtifactId;
|
|
178
181
|
if (sourceArtifactId !== undefined && (capability !== 'image' || !ARTIFACT_ID.test(sourceArtifactId))) fail('IRIS_TASK_INPUT_INVALID', '改图 sourceArtifactId 必须是合法 Artifact ID');
|
|
179
182
|
const now = new Date().toISOString();
|
|
180
|
-
return
|
|
183
|
+
return {
|
|
181
184
|
schemaVersion: 0,
|
|
182
185
|
semanticsVersion: 2,
|
|
183
|
-
id
|
|
186
|
+
id,
|
|
184
187
|
capability,
|
|
185
188
|
status: 'running',
|
|
186
189
|
phase: 'queued',
|
|
@@ -197,7 +200,50 @@ export function createCoreTask(dataRoot, input = {}) {
|
|
|
197
200
|
// D4 retry as new task:显式单向关系(新任务指向旧任务),绝不复制 Prompt。
|
|
198
201
|
...(retriedFrom !== undefined ? { retriedFrom } : {}),
|
|
199
202
|
...(sourceArtifactId !== undefined ? { sourceArtifactId } : {})
|
|
200
|
-
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createCoreTask(dataRoot, input = {}) {
|
|
207
|
+
return writeRecord(dataRoot, newTaskRecord(input, 'task_' + crypto.randomBytes(12).toString('hex')));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Flow 只能接回自己持久预留的 Task;原有 Task 没有该字段,读取语义不变。 */
|
|
211
|
+
export function assertFlowTaskMatches(task, run) {
|
|
212
|
+
const providerId = parseModelRef(run.input.model_ref).providerId;
|
|
213
|
+
if (task.id !== run.edit.taskId || task.capability !== 'image' || task.sourceArtifactId !== run.source.artifactId
|
|
214
|
+
|| !validFlowBinding(task.flowBinding) || task.flowBinding.runId !== run.id
|
|
215
|
+
|| task.flowBinding.inputDigest !== run.inputDigest
|
|
216
|
+
|| task.attempts.some(attempt => attempt.providerId !== providerId
|
|
217
|
+
|| attempt.model !== run.input.model_ref || attempt.providerBinding !== run.providerBinding)
|
|
218
|
+
|| (task.attempts.length && (task.providerId !== providerId || task.modelRef !== run.input.model_ref || task.providerBinding !== run.providerBinding))) {
|
|
219
|
+
fail('IRIS_FLOW_TASK_CONFLICT', '流程与原 Task 的身份、输入或模型绑定冲突');
|
|
220
|
+
}
|
|
221
|
+
return task;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function findFlowTask(dataRoot, run) {
|
|
225
|
+
for (const relative of [STORE_DIR, STORE_DIR + '/' + STORE_VERSION, STORE_DIR + '/' + STORE_VERSION + '/tasks']) {
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.lstatSync(path.join(dataRoot, relative));
|
|
228
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('IRIS_FLOW_TASK_CONFLICT', '流程 Task 目录不安全');
|
|
229
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
230
|
+
}
|
|
231
|
+
try { fs.lstatSync(taskFile(dataRoot, run.edit.taskId)); }
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error.code !== 'ENOENT') throw error;
|
|
234
|
+
if (run.edit.claimed) fail('IRIS_FLOW_TASK_CONFLICT', '流程已建立的 Task 丢失,禁止重建或重新生成');
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return assertFlowTaskMatches(readRecord(dataRoot, run.edit.taskId), run);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 仅从已提交的 Run 读取 ID;排他发布,新旧身份均不能覆盖。 */
|
|
241
|
+
export function claimFlowTask(dataRoot, runId) {
|
|
242
|
+
const run = readFlowRun(dataRoot, runId);
|
|
243
|
+
const previous = findFlowTask(dataRoot, run);
|
|
244
|
+
if (previous) return previous;
|
|
245
|
+
return writeRecord(dataRoot, { ...newTaskRecord({ capability: 'image', sourceArtifactId: run.source.artifactId }, run.edit.taskId),
|
|
246
|
+
flowBinding: flowTaskBinding(run) }, true);
|
|
201
247
|
}
|
|
202
248
|
|
|
203
249
|
export function inspectCoreTask(dataRoot, id) {
|
|
@@ -366,7 +412,7 @@ export function activateCoreWatch(dataRoot, taskId) {
|
|
|
366
412
|
});
|
|
367
413
|
}
|
|
368
414
|
|
|
369
|
-
export function recordCorePollResult(dataRoot, taskId, result) {
|
|
415
|
+
export function recordCorePollResult(dataRoot, taskId, result, { deliveryCache = false } = {}) {
|
|
370
416
|
return mutate(dataRoot, taskId, (task) => {
|
|
371
417
|
if (task.acceptance !== 'accepted' || !task.remoteTaskId) {
|
|
372
418
|
fail('IRIS_TASK_NOT_OBSERVABLE', 'Task 缺少可观察的远端受理事实');
|
|
@@ -387,6 +433,7 @@ export function recordCorePollResult(dataRoot, taskId, result) {
|
|
|
387
433
|
task.phase = 'running';
|
|
388
434
|
task.outcome = 'succeeded';
|
|
389
435
|
task.deliveryState = 'pending';
|
|
436
|
+
if (deliveryCache) task.deliveryCache = true;
|
|
390
437
|
delete task.lastError;
|
|
391
438
|
return;
|
|
392
439
|
}
|
|
@@ -524,6 +571,13 @@ export function recordCoreCancelResult(dataRoot, taskId, result) {
|
|
|
524
571
|
*/
|
|
525
572
|
export function recoverCoreTask(dataRoot, taskId) {
|
|
526
573
|
let current = readRecord(dataRoot, taskId);
|
|
574
|
+
// Flow 的异步完成结果同样先缓存,再记录 poll;中断时只恢复原结果,不重新查询/生成。
|
|
575
|
+
if (current.flowBinding && current.acceptance === 'accepted' && current.remoteTaskId
|
|
576
|
+
&& current.outcome === 'none' && current.cancelState === 'none'
|
|
577
|
+
&& readDeliveryCache(dataRoot, current, { allowAbsent: true })) {
|
|
578
|
+
recordCorePollResult(dataRoot, taskId, { kind: 'succeeded' }, { deliveryCache: true });
|
|
579
|
+
current = readRecord(dataRoot, taskId);
|
|
580
|
+
}
|
|
527
581
|
if (current.phase === 'submitting' && current.acceptance === 'none') {
|
|
528
582
|
const cache = readDeliveryCache(dataRoot, current, { allowAbsent: true });
|
|
529
583
|
if (cache) {
|
|
@@ -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
|
@@ -421,7 +421,18 @@ export function hostDoctor(host, evidence = {}, options = {}) {
|
|
|
421
421
|
+ (missingSkills.length ? ':' + missingSkills.join(', ') : ''),
|
|
422
422
|
'工具仍可直接调用;检查 DSH Skill registry 和插件启动日志。'));
|
|
423
423
|
|
|
424
|
-
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
|
+
}
|
|
425
436
|
capabilityCheck(checks, snapshot, 'attachments', 'attachments', '附件读写');
|
|
426
437
|
capabilityCheck(checks, snapshot, 'sessions', 'sessions', '会话查询');
|
|
427
438
|
capabilityCheck(checks, snapshot, 'text-model', 'textModel', '宿主文本模型');
|