@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +28 -19
  3. package/README.md +27 -18
  4. package/bin/dsh-iris.js +140 -26
  5. package/docs/AGENT_QUICKSTART.md +5 -5
  6. package/docs/ARCHITECTURE.md +9 -3
  7. package/docs/CLI_MANAGEMENT.md +1 -1
  8. package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
  9. package/docs/CORE_SKILLS.md +62 -0
  10. package/docs/DSH_COMPATIBILITY.md +4 -4
  11. package/docs/DSH_CORE_MIGRATION.md +4 -2
  12. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  13. package/docs/DSH_RUNTIME.md +36 -0
  14. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  15. package/docs/HEADLESS_CLI.md +11 -3
  16. package/docs/HOST_DOCTOR.md +13 -1
  17. package/docs/IMAGE_FLOW.md +71 -0
  18. package/docs/LOCAL_API.md +60 -0
  19. package/docs/LOCAL_API_CONTRACT.md +115 -0
  20. package/docs/LOCAL_CREATION.md +50 -0
  21. package/docs/PROVIDER_HEALTH.md +1 -1
  22. package/docs/ROADMAP.md +14 -2
  23. package/docs/SECURITY.md +9 -1
  24. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  25. package/docs/VISION_CLI.md +1 -1
  26. package/docs/WEB_PROTOTYPE.md +47 -0
  27. package/docs/releases/0.2.7.md +62 -0
  28. package/lib/actions.js +79 -68
  29. package/lib/api.js +38 -38
  30. package/lib/application-runtime.js +186 -0
  31. package/lib/client.js +89 -3
  32. package/lib/core-artifact-store.js +5 -0
  33. package/lib/core-maintenance.js +25 -2
  34. package/lib/core-runtime.js +19 -0
  35. package/lib/core-tasks.js +62 -8
  36. package/lib/creation-request-store.js +159 -0
  37. package/lib/doctor.js +12 -1
  38. package/lib/dsh-core-adapter.js +666 -572
  39. package/lib/dsh-host-adapter.js +4 -1
  40. package/lib/flow-store.js +195 -0
  41. package/lib/image-flow.js +188 -0
  42. package/lib/index.js +59 -53
  43. package/lib/local-api-projection.js +27 -0
  44. package/lib/local-api.js +381 -0
  45. package/lib/local-creation.js +85 -0
  46. package/lib/private-storage.js +12 -0
  47. package/lib/provider-task-runner.js +22 -2
  48. package/lib/render-address.js +35 -0
  49. package/lib/render.js +52 -13
  50. package/lib/skill-installation.js +199 -0
  51. package/lib/workbench-artifacts.js +1 -2
  52. package/lib/workbench-management.js +8 -9
  53. package/package.json +3 -2
  54. package/skills/iris-core/SKILL.md +40 -0
  55. package/skills/iris-core/agents/openai.yaml +4 -0
  56. package/skills/iris-core/references/cli.md +95 -0
  57. package/skills/iris-core/references/tasks.md +58 -0
  58. 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
- atomicWritePrivate(taskFile(dataRoot, record.id), JSON.stringify(record, null, 2) + '\n');
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
- export function createCoreTask(dataRoot, input = {}) {
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 writeRecord(dataRoot, {
183
+ return {
181
184
  schemaVersion: 0,
182
185
  semanticsVersion: 2,
183
- id: 'task_' + crypto.randomBytes(12).toString('hex'),
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', '宿主文本模型');