@mokuyoaxis/dsh-iris 0.1.3 → 0.1.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.
Files changed (51) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
  2. package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
  3. package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
  4. package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
  5. package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
  6. package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
  7. package/CHANGELOG.md +24 -2
  8. package/README.en.md +29 -7
  9. package/README.md +29 -7
  10. package/docs/ARCHITECTURE.md +27 -13
  11. package/docs/ARTIFACT_LIBRARY.md +24 -0
  12. package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
  13. package/docs/HOST_DOCTOR.md +47 -0
  14. package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
  15. package/docs/PROVIDER_HEALTH.md +39 -0
  16. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  17. package/docs/ROADMAP.md +18 -8
  18. package/docs/SECURITY.md +6 -2
  19. package/docs/TASK_SEMANTICS.md +3 -2
  20. package/docs/assets/diagrams/iris-architecture.png +0 -0
  21. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  22. package/docs/releases/0.1.4.md +59 -0
  23. package/lib/actions.js +357 -221
  24. package/lib/adapters.js +7 -7
  25. package/lib/api.js +30 -11
  26. package/lib/artifacts.js +239 -0
  27. package/lib/bundled-skills.js +5 -5
  28. package/lib/client.js +370 -55
  29. package/lib/config.js +192 -9
  30. package/lib/doctor.js +175 -4
  31. package/lib/dsh-host-adapter.js +189 -0
  32. package/lib/host-contract.js +131 -0
  33. package/lib/host-runtime.js +156 -0
  34. package/lib/index.js +97 -113
  35. package/lib/locate.js +1 -1
  36. package/lib/media.js +16 -6
  37. package/lib/prompt-optimizer.js +10 -20
  38. package/lib/provider-adapter.js +371 -0
  39. package/lib/provider-adapters.js +279 -0
  40. package/lib/provider-health.js +59 -0
  41. package/lib/render.js +43 -12
  42. package/lib/tasks.js +29 -5
  43. package/lib/vision.js +23 -49
  44. package/package.json +14 -8
  45. package/user_guide.md +42 -2
  46. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  47. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  48. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  49. package/docs/releases/0.1.1.md +0 -51
  50. package/docs/releases/0.1.2.md +0 -26
  51. package/docs/releases/0.1.3.md +0 -30
package/lib/adapters.js CHANGED
@@ -236,11 +236,11 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
236
236
  const TERMINAL = new Set(['SUCCEEDED', 'FAILED', 'CANCELED', 'UNKNOWN']);
237
237
 
238
238
  /** @returns {{done:boolean, ok:boolean, urls:string[], message?:string}} */
239
- export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL }) {
239
+ export async function pollTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal }) {
240
240
  const apiBase = dashscopeApiBase(baseUrl);
241
241
  const res = await fetch(`${apiBase}/tasks/${encodeURIComponent(remoteTaskId)}`, {
242
242
  headers: authHeaders(key),
243
- signal: timeoutSignal(timeoutMs)
243
+ signal: timeoutSignal(timeoutMs, signal)
244
244
  });
245
245
  const body = await readJson(res);
246
246
  if (!res.ok) throw providerError(res.status, body, '查询任务失败');
@@ -323,10 +323,10 @@ export async function openAiGenerateImage({ key, baseUrl, model, prompt, size =
323
323
  * 免费只读;返回 [{ id }] 原样(分类由 models.js 规则负责)。
324
324
  * @returns {Promise<string[]>}
325
325
  */
326
- export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT }) {
326
+ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal }) {
327
327
  const base = String(baseUrl || '').trim().replace(/\/+$/, '');
328
328
  if (!base) throw new Error('iris: 供应商缺 baseUrl,无法发现模型');
329
- const res = await fetch(`${base}/models`, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs) });
329
+ const res = await fetch(`${base}/models`, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
330
330
  const body = await readJson(res);
331
331
  if (!res.ok) throw providerError(res.status, body, '模型发现失败');
332
332
  const items = (body && Array.isArray(body.data) && body.data) || (Array.isArray(body && body.models) ? body.models : []);
@@ -476,11 +476,11 @@ export function transcriptionText(payload) {
476
476
  }
477
477
 
478
478
  /** 轮询 ASR 任务;正式结果位于 results[].transcription_url 指向的 JSON。 */
479
- export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL }) {
479
+ export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeoutMs = T_POLL, signal }) {
480
480
  const apiBase = dashscopeApiBase(baseUrl);
481
481
  const res = await fetch(`${apiBase}/tasks/${encodeURIComponent(remoteTaskId)}`, {
482
482
  headers: authHeaders(key),
483
- signal: timeoutSignal(timeoutMs)
483
+ signal: timeoutSignal(timeoutMs, signal)
484
484
  });
485
485
  const body = await readJson(res);
486
486
  if (!res.ok) throw providerError(res.status, body, '查询转写任务失败');
@@ -499,7 +499,7 @@ export async function pollTranscriptionTask({ key, baseUrl, remoteTaskId, timeou
499
499
  if (typeof result.transcription_text === 'string') text += result.transcription_text;
500
500
  else if (typeof result.text === 'string') text += result.text;
501
501
  if (status === 'SUCCEEDED' && result.subtask_status !== 'FAILED' && typeof result.transcription_url === 'string') {
502
- const transcriptRes = await fetch(result.transcription_url, { signal: timeoutSignal(timeoutMs) });
502
+ const transcriptRes = await fetch(result.transcription_url, { signal: timeoutSignal(timeoutMs, signal) });
503
503
  if (!transcriptRes.ok) throw new Error(`下载转写结果失败:HTTP ${transcriptRes.status}`);
504
504
  const transcriptBody = await readJson(transcriptRes);
505
505
  if (!transcriptBody) throw new Error('下载转写结果失败:响应不是 JSON');
package/lib/api.js CHANGED
@@ -13,13 +13,16 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import crypto from 'node:crypto';
16
- import { irisHome, capabilitiesOf, onChange as onConfigChange } from './config.js';
16
+ import { irisHome, capabilitiesOf, onChange as onConfigChange, providerHealthSnapshot } from './config.js';
17
17
  import { mimeOf, mediaUrl } from './media.js';
18
18
  import * as tasks from './tasks.js';
19
+ import * as artifacts from './artifacts.js';
19
20
  import { runAction } from './actions.js';
20
21
  import { chmodPrivateFile, ensurePrivateDir } from './private-storage.js';
21
22
  import { inferMediaProtocol } from './provider-protocol.js';
22
23
  import { deriveUserState } from './task-semantics.js';
24
+ import { hostDoctor } from './doctor.js';
25
+ import { hostRuntimeEvidence, recordHostClient } from './host-runtime.js';
23
26
  import { optimizePrompt } from './prompt-optimizer.js';
24
27
  import {
25
28
  importPromptOptimizerConfig,
@@ -149,11 +152,14 @@ export function buildState() {
149
152
  const terminal = all.filter((t) => ['succeeded', 'failed', 'canceled'].includes(stateOf(t))
150
153
  || (needsAttention(t) && acknowledged(t)) || t.expired);
151
154
  const recent = terminal.slice(0, 100).map(taskPublic);
155
+ const artifactSummary = artifacts.page({ offset: 0, limit: 6 });
152
156
 
153
157
  return {
154
158
  stateEpoch: STATE_EPOCH,
155
159
  stateRevision: ++stateRevision,
156
160
  providers: providers.map(providerPublic),
161
+ health: providerHealthSnapshot(),
162
+ artifacts: { total: artifactSummary.total, recent: artifactSummary.items },
157
163
  tasks: {
158
164
  recent,
159
165
  recentTotal: terminal.length, // 泡泡「查看全部 (N)」计数用(不受 slice 截断)
@@ -276,7 +282,7 @@ let sseBusDisposers = [];
276
282
  function bindChangeBus() {
277
283
  if (sseBound) return;
278
284
  sseBound = true;
279
- sseBusDisposers = [tasks.onChange(scheduleBroadcast), onConfigChange(scheduleBroadcast)];
285
+ sseBusDisposers = [tasks.onChange(scheduleBroadcast), artifacts.onChange(scheduleBroadcast), onConfigChange(scheduleBroadcast)];
280
286
  }
281
287
  function unbindChangeBus() {
282
288
  for (const d of sseBusDisposers) {
@@ -316,22 +322,26 @@ function serveSse(res) {
316
322
  }
317
323
 
318
324
  /** webServer 前缀路由 handler:状态、任务、动作与提示词优化器 API。 */
319
- export function serveApi(req, res, ctx) {
325
+ export function serveApi(req, res, host) {
320
326
  try {
321
327
  const method = String(req.method || 'GET').toUpperCase();
322
328
  const urlPath = String(req.url || '').split('?')[0];
323
329
 
324
330
  // POST /iris/api/actions/:name —— GUI 直连触发工具执行
325
331
  if (method === 'POST') {
332
+ if (urlPath === '/iris/api/host-client') {
333
+ if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
334
+ return handleJsonPost(req, res, (body) => recordHostClient(body));
335
+ }
326
336
  // 文件选择器 L2:上传本地文件字节 → 存 uploads/ → 返回路径(独立大 body 上限)
327
337
  if (urlPath === '/iris/api/upload') {
328
338
  return handleUpload(req, res);
329
339
  }
330
- const promptMatch = typeof ctx === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
331
- if (promptMatch) return handlePromptOptimizer(ctx, promptMatch[1], req, res);
332
- const actionMatch = typeof ctx === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
340
+ const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
341
+ if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
342
+ const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
333
343
  if (!actionMatch) { return sendJson(res, 405, { error: 'method not allowed' }); }
334
- return handleAction(ctx, actionMatch[1], req, res);
344
+ return handleAction(host, actionMatch[1], req, res);
335
345
  }
336
346
  // GET/HEAD 原逻辑
337
347
  if (method !== 'GET' && method !== 'HEAD') {
@@ -345,6 +355,15 @@ export function serveApi(req, res, ctx) {
345
355
  if (urlPath === '/iris/api/prompt-optimizer/config') {
346
356
  return sendJson(res, 200, method === 'HEAD' ? {} : loadPromptOptimizerConfig());
347
357
  }
358
+ if (urlPath === '/iris/api/doctor') {
359
+ if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
360
+ return sendJson(res, 200, method === 'HEAD' ? {} : hostDoctor(host, hostRuntimeEvidence()));
361
+ }
362
+ if (urlPath === '/iris/api/artifacts') {
363
+ const query = new URL(req.url || '', 'http://iris.local').searchParams;
364
+ const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
365
+ return sendJson(res, 200, method === 'HEAD' ? {} : page);
366
+ }
348
367
  const taskMatch = urlPath.match(/^\/iris\/api\/task\/([^/]+)$/);
349
368
  if (taskMatch) {
350
369
  return serveTaskDetail(taskMatch[1], method, res);
@@ -413,13 +432,13 @@ function handleJsonPost(req, res, execute) {
413
432
  req.on('error', () => finish(400, { error: 'bad request' }));
414
433
  }
415
434
 
416
- function handleAction(ctx, name, req, res) {
417
- return handleJsonPost(req, res, (args, signal) => runAction(ctx, name, args, { signal }));
435
+ function handleAction(host, name, req, res) {
436
+ return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal }));
418
437
  }
419
438
 
420
- function handlePromptOptimizer(ctx, operation, req, res) {
439
+ function handlePromptOptimizer(host, operation, req, res) {
421
440
  return handleJsonPost(req, res, async (body, signal) => {
422
- if (operation === 'optimize') return optimizePrompt(ctx, body, { signal });
441
+ if (operation === 'optimize') return optimizePrompt(host, body, { signal });
423
442
  if (operation === 'import') {
424
443
  const imported = body && body.config !== undefined ? body.config : body;
425
444
  return { ok: true, ...importPromptOptimizerConfig(imported) };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+ /**
3
+ * Iris 作品库 v0。
4
+ *
5
+ * 任务历史与作品生命周期相互独立:清除 tasks.json 不会让 outputs/ 中的作品
6
+ * 从 UI 消失。这里只记录展示、分页和随机令牌授权所需的最小字段,不保留
7
+ * prompt、Provider、Model 或任务关系;完整 Artifact Manifest 留给 0.2.0。
8
+ */
9
+ import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { irisHome } from './config.js';
13
+ import { atomicWritePrivate, chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
14
+
15
+ const FILE = () => path.join(irisHome(), 'artifacts.json');
16
+ const OUTPUTS = () => path.join(irisHome(), 'outputs');
17
+ const MIME = {
18
+ '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
19
+ '.mkv': 'video/x-matroska', '.avi': 'video/x-msvideo', '.mpeg': 'video/mpeg', '.mpg': 'video/mpeg',
20
+ '.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.m4a': 'audio/mp4',
21
+ '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.opus': 'audio/ogg', '.flac': 'audio/flac',
22
+ '.avif': 'image/avif', '.bmp': 'image/bmp',
23
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
24
+ '.webp': 'image/webp', '.gif': 'image/gif'
25
+ };
26
+
27
+ let cache = null;
28
+ const changeListeners = new Set();
29
+
30
+ export function onChange(fn) {
31
+ changeListeners.add(fn);
32
+ return () => changeListeners.delete(fn);
33
+ }
34
+
35
+ function emitChange() {
36
+ for (const fn of [...changeListeners]) {
37
+ try { fn(); } catch (_) { /* 一个监听者不能阻断作品落盘 */ }
38
+ }
39
+ }
40
+
41
+ function persist() {
42
+ atomicWritePrivate(FILE(), JSON.stringify({ version: 1, artifacts: cache.artifacts }, null, 2));
43
+ emitChange();
44
+ }
45
+
46
+ function mimeFor(name) {
47
+ return MIME[path.extname(String(name)).toLowerCase()] || '';
48
+ }
49
+
50
+ function safeOutputFile(name) {
51
+ const value = String(name || '');
52
+ return value && path.basename(value) === value && value !== '.' && value !== '..' && mimeFor(value);
53
+ }
54
+
55
+ function outputStat(name) {
56
+ if (!safeOutputFile(name)) return null;
57
+ const abs = path.join(OUTPUTS(), name);
58
+ try {
59
+ const stat = fs.lstatSync(abs);
60
+ return stat.isFile() && !stat.isSymbolicLink() ? { abs, stat } : null;
61
+ } catch (_) {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function newEntry(name, stat) {
67
+ return {
68
+ id: 'a_' + crypto.randomBytes(12).toString('hex'),
69
+ file: name,
70
+ token: crypto.randomBytes(16).toString('hex'),
71
+ mime: mimeFor(name),
72
+ size: stat.size,
73
+ createdAt: new Date(stat.mtimeMs || Date.now()).toISOString()
74
+ };
75
+ }
76
+
77
+ function validateStored(value) {
78
+ if (!value || typeof value !== 'object' || !Array.isArray(value.artifacts)) throw new Error('artifacts 必须是数组');
79
+ const ids = new Set();
80
+ const files = new Set();
81
+ for (const item of value.artifacts) {
82
+ if (!item || typeof item !== 'object' || !String(item.id || '').startsWith('a_')
83
+ || !/^[a-f0-9]{32}$/.test(String(item.token || '')) || !safeOutputFile(item.file)) {
84
+ throw new Error('作品条目格式无效');
85
+ }
86
+ if (ids.has(item.id) || files.has(item.file)) throw new Error('作品条目重复');
87
+ ids.add(item.id);
88
+ files.add(item.file);
89
+ }
90
+ }
91
+
92
+ function adoptOutputs() {
93
+ ensurePrivateDir(OUTPUTS());
94
+ const known = new Set(cache.artifacts.map((item) => item.file));
95
+ let added = 0;
96
+ for (const name of fs.readdirSync(OUTPUTS())) {
97
+ if (known.has(name)) continue;
98
+ const hit = outputStat(name);
99
+ if (!hit) continue;
100
+ cache.artifacts.push(newEntry(name, hit.stat));
101
+ known.add(name);
102
+ added++;
103
+ }
104
+ return added;
105
+ }
106
+
107
+ function load() {
108
+ if (cache) return cache;
109
+ const file = FILE();
110
+ if (!fs.existsSync(file)) {
111
+ cache = { version: 1, artifacts: [] };
112
+ adoptOutputs(); // 升级时一次性接回现有 outputs;不读取任务历史。
113
+ persist();
114
+ return cache;
115
+ }
116
+ try {
117
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
118
+ validateStored(raw);
119
+ cache = { version: 1, artifacts: raw.artifacts.map((item) => ({ ...item })) };
120
+ } catch (error) {
121
+ const backup = privateSibling(file, 'corrupted');
122
+ try {
123
+ fs.renameSync(file, backup);
124
+ chmodPrivateFile(backup);
125
+ } catch (_) { /* 隔离失败仍继续用文件重建最小索引 */ }
126
+ console.error('[iris] artifacts.json 已损坏,已隔离为 ' + backup + ':', error && error.message);
127
+ cache = { version: 1, artifacts: [] };
128
+ adoptOutputs();
129
+ persist();
130
+ }
131
+ return cache;
132
+ }
133
+
134
+ export function resetCache() {
135
+ cache = null;
136
+ }
137
+
138
+ export function all() {
139
+ return [...load().artifacts].sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
140
+ }
141
+
142
+ export function artifactUrl(entry) {
143
+ const base = String(process.env.DSH_WEB_BASE || 'http://127.0.0.1:3080').replace(/\/+$/, '');
144
+ return `${base}/iris/media/artifact/${entry.id}/${entry.token}/${encodeURIComponent(entry.file)}`;
145
+ }
146
+
147
+ function publicEntry(item) {
148
+ return {
149
+ id: item.id,
150
+ file: item.file,
151
+ mime: item.mime || mimeFor(item.file),
152
+ size: Number(item.size || 0),
153
+ createdAt: item.createdAt || '',
154
+ url: artifactUrl(item)
155
+ };
156
+ }
157
+
158
+ export function page({ offset = 0, limit = 24 } = {}) {
159
+ const sorted = all();
160
+ const start = Math.max(0, Number.isFinite(Number(offset)) ? Math.trunc(Number(offset)) : 0);
161
+ const size = Math.max(1, Math.min(60, Number.isFinite(Number(limit)) ? Math.trunc(Number(limit)) : 24));
162
+ return { total: sorted.length, offset: start, limit: size, items: sorted.slice(start, start + size).map(publicEntry) };
163
+ }
164
+
165
+ export function register(absPath) {
166
+ const resolved = path.resolve(String(absPath || ''));
167
+ if (path.dirname(resolved) !== path.resolve(OUTPUTS())) throw new Error('作品必须位于 Iris outputs/');
168
+ const name = path.basename(resolved);
169
+ const hit = outputStat(name);
170
+ if (!hit) throw new Error('作品文件不存在、类型不受支持或不是普通文件');
171
+ const data = load();
172
+ const existing = data.artifacts.find((item) => item.file === name);
173
+ if (existing) {
174
+ if (existing.size !== hit.stat.size || existing.mime !== mimeFor(name)) {
175
+ existing.size = hit.stat.size;
176
+ existing.mime = mimeFor(name);
177
+ persist();
178
+ }
179
+ return publicEntry(existing);
180
+ }
181
+ const entry = newEntry(name, hit.stat);
182
+ data.artifacts.push(entry);
183
+ persist();
184
+ return publicEntry(entry);
185
+ }
186
+
187
+ export function reindex() {
188
+ const data = load();
189
+ const before = data.artifacts.length;
190
+ data.artifacts = data.artifacts.filter((item) => outputStat(item.file));
191
+ const missing = before - data.artifacts.length;
192
+ const added = adoptOutputs();
193
+ if (missing || added) persist();
194
+ return { added, missing, total: data.artifacts.length };
195
+ }
196
+
197
+ export function authorize(id, token, name) {
198
+ const item = load().artifacts.find((entry) => entry.id === id && entry.token === token && entry.file === name);
199
+ if (!item) return null;
200
+ const hit = outputStat(item.file);
201
+ if (!hit) return null;
202
+ return { entry: item, abs: hit.abs, size: hit.stat.size };
203
+ }
204
+
205
+ export function remove(id) {
206
+ const data = load();
207
+ const index = data.artifacts.findIndex((item) => item.id === id);
208
+ if (index < 0) return { ok: false, reason: '作品不存在' };
209
+ const item = data.artifacts[index];
210
+ const hit = outputStat(item.file);
211
+ if (hit) fs.rmSync(hit.abs);
212
+ data.artifacts.splice(index, 1);
213
+ persist();
214
+ return { ok: true, file: item.file };
215
+ }
216
+
217
+ export function clear() {
218
+ const data = load();
219
+ const kept = [];
220
+ let deleted = 0;
221
+ let bytes = 0;
222
+ for (const item of data.artifacts) {
223
+ const hit = outputStat(item.file);
224
+ try {
225
+ if (hit) {
226
+ fs.rmSync(hit.abs);
227
+ bytes += hit.stat.size;
228
+ }
229
+ deleted++;
230
+ } catch (_) {
231
+ kept.push(item);
232
+ }
233
+ }
234
+ if (deleted) {
235
+ data.artifacts = kept;
236
+ persist();
237
+ }
238
+ return { deleted, failed: kept.length, bytes };
239
+ }
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { requireHostPort } from './host-contract.js';
6
7
 
7
8
  const SKILL_NAMES = ['iris-verify-ui', 'iris-compose-media'];
8
9
  const SKILL_ROOT = fileURLToPath(new URL('../.dsh/skills/', import.meta.url));
@@ -44,6 +45,7 @@ export function loadBundledSkills() {
44
45
  content: source.slice(frontmatter[0].length).replace(/^\r?\n/, ''),
45
46
  source: 'bundled',
46
47
  provider: 'iris-bundled',
48
+ invocation: Object.freeze({ modelInvocable: true, userInvocable: true }),
47
49
  path: file,
48
50
  resourceBase: Object.freeze({ kind: 'directory', path: directory })
49
51
  });
@@ -51,11 +53,9 @@ export function loadBundledSkills() {
51
53
  }
52
54
 
53
55
  /** 注册为宿主级 runtime Skill;DSH 项目目录中的同名 Skill 仍按原生优先级胜出。 */
54
- export function registerBundledSkills(ctx) {
55
- const registry = ctx && (ctx.skills || (typeof ctx.get === 'function' && ctx.get('skills')));
56
- if (!registry || typeof registry.register !== 'function') {
57
- throw new Error('DSH Skill registry 不可用');
58
- }
56
+ export function registerBundledSkills(host) {
57
+ const registry = requireHostPort(host, 'skills', '注册随包 Skill');
58
+ if (typeof registry.register !== 'function') throw new Error('Host skills 端口缺少 register');
59
59
  const skills = loadBundledSkills();
60
60
  for (const skill of skills) registry.register(skill);
61
61
  return skills.map((skill) => skill.name);