@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.
- package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
- package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
- package/CHANGELOG.md +24 -2
- package/README.en.md +29 -7
- package/README.md +29 -7
- package/docs/ARCHITECTURE.md +27 -13
- package/docs/ARTIFACT_LIBRARY.md +24 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
- package/docs/HOST_DOCTOR.md +47 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
- package/docs/PROVIDER_HEALTH.md +39 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +6 -2
- package/docs/TASK_SEMANTICS.md +3 -2
- package/docs/assets/diagrams/iris-architecture.png +0 -0
- package/docs/assets/diagrams/iris-architecture.svg +5 -5
- package/docs/releases/0.1.4.md +59 -0
- package/lib/actions.js +357 -221
- package/lib/adapters.js +7 -7
- package/lib/api.js +30 -11
- package/lib/artifacts.js +239 -0
- package/lib/bundled-skills.js +5 -5
- package/lib/client.js +370 -55
- package/lib/config.js +192 -9
- package/lib/doctor.js +175 -4
- package/lib/dsh-host-adapter.js +189 -0
- package/lib/host-contract.js +131 -0
- package/lib/host-runtime.js +156 -0
- package/lib/index.js +97 -113
- package/lib/locate.js +1 -1
- package/lib/media.js +16 -6
- package/lib/prompt-optimizer.js +10 -20
- package/lib/provider-adapter.js +371 -0
- package/lib/provider-adapters.js +279 -0
- package/lib/provider-health.js +59 -0
- package/lib/render.js +43 -12
- package/lib/tasks.js +29 -5
- package/lib/vision.js +23 -49
- package/package.json +14 -8
- package/user_guide.md +42 -2
- package/docs/assets/diagrams/iris-architecture.drawio +0 -1
- package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
- package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
- package/docs/releases/0.1.1.md +0 -51
- package/docs/releases/0.1.2.md +0 -26
- package/docs/releases/0.1.3.md +0 -30
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Iris Host Adapter v0 的纯契约原语。
|
|
5
|
+
*
|
|
6
|
+
* 本模块不导入 DSH/Cordis,也不读取宿主服务。具体 Adapter 只能把宿主能力
|
|
7
|
+
* 映射到这些命名端口;Command 不得接收原始 ctx。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const HOST_CONTRACT_VERSION = 0;
|
|
11
|
+
|
|
12
|
+
export const HOST_PORTS = Object.freeze([
|
|
13
|
+
'attachments',
|
|
14
|
+
'browser',
|
|
15
|
+
'clientSlots',
|
|
16
|
+
'routes',
|
|
17
|
+
'sessions',
|
|
18
|
+
'skills',
|
|
19
|
+
'textModel',
|
|
20
|
+
'tools',
|
|
21
|
+
'visionModel'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const HOST_PORT_SET = new Set(HOST_PORTS);
|
|
25
|
+
const HOST_ID = /^[a-z0-9][a-z0-9._-]*$/;
|
|
26
|
+
const UNAVAILABLE_KINDS = new Set(['unavailable', 'incompatible']);
|
|
27
|
+
|
|
28
|
+
function isPlainObject(value) {
|
|
29
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validatePortName(name) {
|
|
33
|
+
if (!HOST_PORT_SET.has(name)) throw new Error(`未知 Host Port:${name}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 建立只读 Host Adapter 描述。ports 中只放真实可调用的能力;缺失或版本不兼容
|
|
38
|
+
* 的能力放入 unavailable,供 Doctor 和用户错误复用同一份原因。
|
|
39
|
+
*/
|
|
40
|
+
export function defineHostAdapter(input) {
|
|
41
|
+
if (!isPlainObject(input)) throw new TypeError('Host Adapter 必须是对象');
|
|
42
|
+
const allowed = new Set(['contractVersion', 'id', 'version', 'ports', 'unavailable']);
|
|
43
|
+
for (const key of Object.keys(input)) {
|
|
44
|
+
if (!allowed.has(key)) throw new Error(`Host Adapter 不允许顶层字段:${key}`);
|
|
45
|
+
}
|
|
46
|
+
const contractVersion = input.contractVersion ?? HOST_CONTRACT_VERSION;
|
|
47
|
+
if (contractVersion !== HOST_CONTRACT_VERSION) {
|
|
48
|
+
throw new Error(`不支持的 Host Adapter 契约版本:${String(contractVersion)}`);
|
|
49
|
+
}
|
|
50
|
+
const id = String(input.id || '').trim();
|
|
51
|
+
if (!HOST_ID.test(id)) throw new Error('Host Adapter id 必须是小写稳定标识');
|
|
52
|
+
const version = String(input.version || 'unknown').trim() || 'unknown';
|
|
53
|
+
const supplied = input.ports ?? {};
|
|
54
|
+
const missing = input.unavailable ?? {};
|
|
55
|
+
if (!isPlainObject(supplied) || !isPlainObject(missing)) {
|
|
56
|
+
throw new TypeError('Host Adapter ports/unavailable 必须是对象');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ports = {};
|
|
60
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
61
|
+
validatePortName(name);
|
|
62
|
+
if (!isPlainObject(value)) throw new TypeError(`Host Port ${name} 必须是方法对象`);
|
|
63
|
+
ports[name] = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const unavailable = {};
|
|
67
|
+
for (const [name, value] of Object.entries(missing)) {
|
|
68
|
+
validatePortName(name);
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(ports, name)) {
|
|
70
|
+
throw new Error(`Host Port ${name} 不能同时可用和不可用`);
|
|
71
|
+
}
|
|
72
|
+
if (!isPlainObject(value) || !UNAVAILABLE_KINDS.has(value.kind) || !String(value.reason || '').trim()) {
|
|
73
|
+
throw new TypeError(`Host Port ${name} 的不可用记录必须包含 kind 和 reason`);
|
|
74
|
+
}
|
|
75
|
+
unavailable[name] = Object.freeze({ kind: value.kind, reason: String(value.reason).trim() });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
contractVersion,
|
|
80
|
+
id,
|
|
81
|
+
version,
|
|
82
|
+
ports: Object.freeze(ports),
|
|
83
|
+
unavailable: Object.freeze(unavailable)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function hasHostPort(adapter, name) {
|
|
88
|
+
validatePortName(name);
|
|
89
|
+
return Boolean(adapter && adapter.ports && Object.prototype.hasOwnProperty.call(adapter.ports, name));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class HostCapabilityError extends Error {
|
|
93
|
+
constructor(adapter, name, operation) {
|
|
94
|
+
validatePortName(name);
|
|
95
|
+
const detail = adapter && adapter.unavailable && adapter.unavailable[name];
|
|
96
|
+
const host = adapter && adapter.id ? adapter.id : 'unknown-host';
|
|
97
|
+
const action = String(operation || '当前操作');
|
|
98
|
+
const suffix = detail && detail.reason ? `:${detail.reason}` : '';
|
|
99
|
+
super(`Iris ${action}需要 Host Port ${name},但 ${host} 未提供${suffix}`);
|
|
100
|
+
this.name = 'HostCapabilityError';
|
|
101
|
+
this.code = detail && detail.kind === 'incompatible'
|
|
102
|
+
? 'IRIS_HOST_CAPABILITY_INCOMPATIBLE'
|
|
103
|
+
: 'IRIS_HOST_CAPABILITY_UNAVAILABLE';
|
|
104
|
+
this.hostId = host;
|
|
105
|
+
this.capability = name;
|
|
106
|
+
this.operation = action;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function requireHostPort(adapter, name, operation) {
|
|
111
|
+
if (!hasHostPort(adapter, name)) throw new HostCapabilityError(adapter, name, operation);
|
|
112
|
+
return adapter.ports[name];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 只返回可序列化能力事实;不得把宿主 live object 暴露给 Doctor/API。 */
|
|
116
|
+
export function hostCapabilitySnapshot(adapter) {
|
|
117
|
+
const capabilities = {};
|
|
118
|
+
for (const name of HOST_PORTS) {
|
|
119
|
+
const available = hasHostPort(adapter, name);
|
|
120
|
+
const detail = !available && adapter && adapter.unavailable ? adapter.unavailable[name] : undefined;
|
|
121
|
+
capabilities[name] = available
|
|
122
|
+
? Object.freeze({ status: 'available' })
|
|
123
|
+
: Object.freeze({ status: detail && detail.kind === 'incompatible' ? 'incompatible' : 'unavailable',
|
|
124
|
+
...(detail && detail.reason ? { reason: detail.reason } : {}) });
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
contractVersion: HOST_CONTRACT_VERSION,
|
|
128
|
+
host: Object.freeze({ id: adapter && adapter.id || 'unknown-host', version: adapter && adapter.version || 'unknown' }),
|
|
129
|
+
capabilities: Object.freeze(capabilities)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host Doctor 的运行时证据账本。
|
|
5
|
+
*
|
|
6
|
+
* 只记录 Iris 自己完成的注册事实和浏览器握手标量;不保存 ctx、service、
|
|
7
|
+
* API Key、会话、附件或其他 live object。
|
|
8
|
+
*/
|
|
9
|
+
export const IRIS_PLUGIN_ID = '@mokuyoaxis/dsh-iris';
|
|
10
|
+
export const HOST_CLIENT_PROTOCOL_VERSION = 0;
|
|
11
|
+
|
|
12
|
+
export const EXPECTED_IRIS_TOOLS = Object.freeze([
|
|
13
|
+
'iris_draw_image',
|
|
14
|
+
'iris_generate_video',
|
|
15
|
+
'iris_speak_text',
|
|
16
|
+
'iris_transcribe_audio',
|
|
17
|
+
'iris_task_status',
|
|
18
|
+
'iris_look_at_image',
|
|
19
|
+
'iris_relook_attachment',
|
|
20
|
+
'iris_crop',
|
|
21
|
+
'iris_pixel_diff',
|
|
22
|
+
'iris_locate',
|
|
23
|
+
'iris_html_screenshot',
|
|
24
|
+
'iris_long_ocr',
|
|
25
|
+
'iris_video_frames',
|
|
26
|
+
'iris_media_summarize'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const EXPECTED_IRIS_SKILLS = Object.freeze([
|
|
30
|
+
'iris-verify-ui',
|
|
31
|
+
'iris-compose-media'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export const EXPECTED_IRIS_ROUTES = Object.freeze([
|
|
35
|
+
'/iris/media',
|
|
36
|
+
'/iris/api',
|
|
37
|
+
'/iris/api/actions',
|
|
38
|
+
'/iris/render'
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const EXPECTED_IRIS_CLIENT_SEATS = Object.freeze([
|
|
42
|
+
'settings.section',
|
|
43
|
+
'conversation.input.right',
|
|
44
|
+
'conversation.input.dock',
|
|
45
|
+
'shell.overlay'
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
function fresh() {
|
|
49
|
+
return {
|
|
50
|
+
server: {
|
|
51
|
+
loaded: false,
|
|
52
|
+
pluginId: IRIS_PLUGIN_ID,
|
|
53
|
+
version: 'unknown',
|
|
54
|
+
startedAt: ''
|
|
55
|
+
},
|
|
56
|
+
tools: new Set(),
|
|
57
|
+
skills: new Set(),
|
|
58
|
+
routes: new Set(),
|
|
59
|
+
client: null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let runtime = fresh();
|
|
64
|
+
|
|
65
|
+
function safeText(value, max = 160) {
|
|
66
|
+
return String(value || '').trim().slice(0, max);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function record(set, values) {
|
|
70
|
+
for (const value of Array.isArray(values) ? values : [values]) {
|
|
71
|
+
const text = safeText(value);
|
|
72
|
+
if (text && set.size < 100) set.add(text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function beginHostRuntime({ pluginId = IRIS_PLUGIN_ID, version = 'unknown' } = {}) {
|
|
77
|
+
runtime = fresh();
|
|
78
|
+
runtime.server = {
|
|
79
|
+
loaded: true,
|
|
80
|
+
pluginId: safeText(pluginId) || IRIS_PLUGIN_ID,
|
|
81
|
+
version: safeText(version) || 'unknown',
|
|
82
|
+
startedAt: new Date().toISOString()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function recordHostTool(name) {
|
|
87
|
+
record(runtime.tools, name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function recordHostSkills(names) {
|
|
91
|
+
record(runtime.skills, names);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function recordHostRoutes(paths) {
|
|
95
|
+
record(runtime.routes, paths);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 接受同源客户端报告。严格限制字段和枚举;报告只是诊断证据,不参与认证。
|
|
100
|
+
*/
|
|
101
|
+
export function recordHostClient(input) {
|
|
102
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
103
|
+
throw new TypeError('Host client report 必须是对象');
|
|
104
|
+
}
|
|
105
|
+
const allowed = new Set(['pluginId', 'version', 'protocolVersion', 'seats']);
|
|
106
|
+
for (const key of Object.keys(input)) {
|
|
107
|
+
if (!allowed.has(key)) throw new TypeError('Host client report 不允许字段:' + key);
|
|
108
|
+
}
|
|
109
|
+
const pluginId = safeText(input.pluginId);
|
|
110
|
+
const version = safeText(input.version);
|
|
111
|
+
const protocolVersion = Number(input.protocolVersion);
|
|
112
|
+
if (pluginId !== IRIS_PLUGIN_ID) throw new TypeError('Host client report 插件身份不匹配');
|
|
113
|
+
if (!version) throw new TypeError('Host client report 缺少版本');
|
|
114
|
+
if (!Number.isSafeInteger(protocolVersion) || protocolVersion < 0) {
|
|
115
|
+
throw new TypeError('Host client report protocolVersion 无效');
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(input.seats) || input.seats.length > 20) {
|
|
118
|
+
throw new TypeError('Host client report seats 无效');
|
|
119
|
+
}
|
|
120
|
+
const incomingSeats = [...new Set(input.seats.map((value) => safeText(value)).filter(Boolean))];
|
|
121
|
+
const invalidSeat = incomingSeats.find((seat) => !EXPECTED_IRIS_CLIENT_SEATS.includes(seat));
|
|
122
|
+
if (invalidSeat) throw new TypeError('Host client report 未知 seat:' + invalidSeat);
|
|
123
|
+
const previous = runtime.client
|
|
124
|
+
&& runtime.client.pluginId === pluginId
|
|
125
|
+
&& runtime.client.version === version
|
|
126
|
+
&& runtime.client.protocolVersion === protocolVersion
|
|
127
|
+
? runtime.client.seats : [];
|
|
128
|
+
// 同一客户端版本的 Slot 回调可能分别发出请求且乱序完成,只允许单调并集。
|
|
129
|
+
const seats = [...new Set(previous.concat(incomingSeats))];
|
|
130
|
+
runtime.client = {
|
|
131
|
+
loaded: true,
|
|
132
|
+
pluginId,
|
|
133
|
+
version,
|
|
134
|
+
protocolVersion,
|
|
135
|
+
seats,
|
|
136
|
+
reportedAt: new Date().toISOString()
|
|
137
|
+
};
|
|
138
|
+
return { ok: true, acceptedSeats: seats.length };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function hostRuntimeEvidence() {
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
server: Object.freeze({ ...runtime.server }),
|
|
144
|
+
tools: Object.freeze([...runtime.tools]),
|
|
145
|
+
skills: Object.freeze([...runtime.skills]),
|
|
146
|
+
routes: Object.freeze([...runtime.routes]),
|
|
147
|
+
client: runtime.client
|
|
148
|
+
? Object.freeze({ ...runtime.client, seats: Object.freeze([...runtime.client.seats]) })
|
|
149
|
+
: null
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 仅供隔离测试复位;生产装载使用 beginHostRuntime。 */
|
|
154
|
+
export function resetHostRuntimeForTests() {
|
|
155
|
+
runtime = fresh();
|
|
156
|
+
}
|