@mokuyoaxis/dsh-iris 0.1.2 → 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 (54) 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 +57 -2
  8. package/README.en.md +43 -7
  9. package/README.md +43 -7
  10. package/bin/dsh-iris.js +23 -0
  11. package/docs/ARCHITECTURE.md +95 -0
  12. package/docs/ARTIFACT_LIBRARY.md +24 -0
  13. package/docs/FAULT_INJECTION.md +26 -0
  14. package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
  15. package/docs/HOST_DOCTOR.md +47 -0
  16. package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
  17. package/docs/PROVIDER_HEALTH.md +39 -0
  18. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +60 -0
  19. package/docs/ROADMAP.md +21 -11
  20. package/docs/SECURITY.md +57 -0
  21. package/docs/TASK_SEMANTICS.md +148 -0
  22. package/docs/assets/diagrams/iris-architecture.png +0 -0
  23. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  24. package/docs/releases/0.1.4.md +59 -0
  25. package/lib/actions.js +860 -290
  26. package/lib/adapters.js +7 -7
  27. package/lib/api.js +110 -18
  28. package/lib/artifacts.js +239 -0
  29. package/lib/bundled-skills.js +5 -5
  30. package/lib/client.js +478 -68
  31. package/lib/config.js +192 -9
  32. package/lib/doctor.js +414 -0
  33. package/lib/dsh-host-adapter.js +189 -0
  34. package/lib/host-contract.js +131 -0
  35. package/lib/host-runtime.js +156 -0
  36. package/lib/index.js +142 -157
  37. package/lib/locate.js +1 -1
  38. package/lib/media.js +16 -6
  39. package/lib/prompt-optimizer.js +10 -20
  40. package/lib/provider-adapter.js +371 -0
  41. package/lib/provider-adapters.js +279 -0
  42. package/lib/provider-contract.js +234 -0
  43. package/lib/provider-health.js +59 -0
  44. package/lib/render.js +43 -12
  45. package/lib/task-semantics.js +209 -0
  46. package/lib/tasks.js +500 -32
  47. package/lib/vision.js +23 -49
  48. package/package.json +19 -8
  49. package/user_guide.md +62 -2
  50. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  51. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  52. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  53. package/docs/releases/0.1.1.md +0 -51
  54. package/docs/releases/0.1.2.md +0 -26
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ /** DSH/Cordis runtime objects are normalized here and nowhere in Iris commands. */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { defineHostAdapter } from './host-contract.js';
7
+ import { BrowserHtmlRenderer } from './render.js';
8
+
9
+ export function detectDshVersion({ entry = process.argv[1] } = {}) {
10
+ if (!entry) return 'unknown';
11
+ let resolved = path.resolve(String(entry));
12
+ try { resolved = fs.realpathSync(resolved); } catch (_) { /* 入口可能尚未物化 */ }
13
+ let current = path.dirname(resolved);
14
+ for (let depth = 0; depth < 6; depth++) {
15
+ try {
16
+ const manifest = JSON.parse(fs.readFileSync(path.join(current, 'package.json'), 'utf8'));
17
+ if (manifest.name === '@deepseek-ai/dsh') return String(manifest.version || 'unknown');
18
+ } catch (_) { /* 当前层不是 DSH 包根目录 */ }
19
+ const parent = path.dirname(current);
20
+ if (parent === current) break;
21
+ current = parent;
22
+ }
23
+ return 'unknown';
24
+ }
25
+
26
+ function property(ctx, name) {
27
+ try { return ctx && ctx[name]; } catch (_) { return undefined; }
28
+ }
29
+
30
+ function service(ctx, name) {
31
+ if (ctx && typeof ctx.get === 'function') {
32
+ try {
33
+ const value = ctx.get(name);
34
+ if (value) return value;
35
+ } catch (_) { /* optional DSH service was not injected */ }
36
+ }
37
+ return property(ctx, name);
38
+ }
39
+
40
+ function unavailableFor(value, reason) {
41
+ return { kind: value ? 'incompatible' : 'unavailable', reason };
42
+ }
43
+
44
+ function collectImageAttachments(nodes) {
45
+ const out = [];
46
+ const ids = new Set();
47
+ const seen = new Set();
48
+ const stack = Array.isArray(nodes) ? [...nodes] : [nodes];
49
+ let scanned = 0;
50
+ while (stack.length && scanned < 8000) {
51
+ const node = stack.pop();
52
+ scanned++;
53
+ if (!node || typeof node !== 'object' || seen.has(node)) continue;
54
+ seen.add(node);
55
+ const ref = node.attachment && typeof node.attachment === 'object'
56
+ ? node.attachment
57
+ : (node.attachmentId && node.mediaType ? node : null);
58
+ if (ref && typeof ref.attachmentId === 'string'
59
+ && typeof ref.mediaType === 'string' && ref.mediaType.startsWith('image/')
60
+ && !ids.has(ref.attachmentId)) {
61
+ ids.add(ref.attachmentId);
62
+ out.push(Object.freeze({
63
+ attachmentId: ref.attachmentId,
64
+ mediaType: ref.mediaType,
65
+ name: typeof ref.name === 'string' ? ref.name : ''
66
+ }));
67
+ }
68
+ for (const value of Object.values(node)) if (value && typeof value === 'object') stack.push(value);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ function registerWithEffect(ctx, registry, definition) {
74
+ if (typeof ctx.effect === 'function') return ctx.effect(() => registry.register(definition));
75
+ return registry.register(definition);
76
+ }
77
+
78
+ function dshVisionPort(llm) {
79
+ return Object.freeze({
80
+ id: 'dsh-global',
81
+ model: '全局视觉模型',
82
+ async analyze({ question, ref, signal }) {
83
+ const chunks = [];
84
+ for await (const chunk of llm.stream({
85
+ sessionId: undefined,
86
+ provider: undefined,
87
+ model: undefined,
88
+ signal,
89
+ messages: [{
90
+ role: 'user',
91
+ content: [
92
+ { type: 'text', text: question },
93
+ { type: 'image', attachment: ref }
94
+ ]
95
+ }]
96
+ })) {
97
+ const text = typeof chunk?.delta === 'string'
98
+ ? chunk.delta
99
+ : (chunk?.type === 'text-delta' ? String(chunk.text || '') : '');
100
+ if (text) chunks.push(text);
101
+ if (chunks.reduce((sum, item) => sum + item.length, 0) > 6000) break;
102
+ }
103
+ return chunks.join('').trim();
104
+ }
105
+ });
106
+ }
107
+
108
+ /** Build a fresh capability snapshot so optional DSH services can arrive after plugin apply(). */
109
+ export function createDshHostAdapter(ctx, { version = detectDshVersion() } = {}) {
110
+ if (!ctx || typeof ctx !== 'object') throw new TypeError('DSH Host Adapter 需要宿主 ctx');
111
+ const ports = {};
112
+ const unavailable = {};
113
+
114
+ const attachments = service(ctx, 'attachments');
115
+ if (attachments && typeof attachments.saveImage === 'function' && typeof attachments.readImage === 'function') {
116
+ ports.attachments = Object.freeze({
117
+ saveImage: (input) => attachments.saveImage(input),
118
+ readImage: (ref, signal) => attachments.readImage(ref, signal)
119
+ });
120
+ } else unavailable.attachments = unavailableFor(attachments, 'DSH attachments 需要 saveImage/readImage');
121
+
122
+ const browser = service(ctx, 'browser');
123
+ if (browser && ['open', 'openUrl', 'screenshot', 'close'].every((name) => typeof browser[name] === 'function')) {
124
+ const renderer = new BrowserHtmlRenderer({ browser });
125
+ ports.browser = Object.freeze({
126
+ async renderHtml(input) {
127
+ const { png } = await renderer.render(input);
128
+ return { bytes: new Uint8Array(png), mediaType: 'image/png' };
129
+ }
130
+ });
131
+ } else unavailable.browser = unavailableFor(browser, 'DSH browser 需要 open/openUrl/screenshot/close');
132
+
133
+ const slots = service(ctx, 'slots');
134
+ if (slots && typeof slots.inject === 'function' && typeof slots.register === 'function') {
135
+ ports.clientSlots = Object.freeze({
136
+ inject: (seat, callback) => slots.inject(seat, callback),
137
+ register: (definition, component) => slots.register(definition, component)
138
+ });
139
+ } else unavailable.clientSlots = unavailableFor(slots, 'DSH client slots 需要 inject/register');
140
+
141
+ const routes = service(ctx, 'webServer') || service(ctx, 'httpServer');
142
+ if (routes && typeof routes.register === 'function') {
143
+ ports.routes = Object.freeze({ register: (definition) => routes.register(definition) });
144
+ } else unavailable.routes = unavailableFor(routes, 'DSH webServer/httpServer 需要 register');
145
+
146
+ const sessions = service(ctx, 'sessionQuery');
147
+ if (sessions && typeof sessions.readSession === 'function') {
148
+ ports.sessions = Object.freeze({
149
+ readSession: (id) => sessions.readSession(id),
150
+ async listImageAttachments(id) {
151
+ const record = await sessions.readSession(id);
152
+ return collectImageAttachments(record && record.events);
153
+ },
154
+ async findImageAttachment(id, attachmentId) {
155
+ return (await this.listImageAttachments(id)).find((item) => item.attachmentId === attachmentId) || null;
156
+ }
157
+ });
158
+ } else unavailable.sessions = unavailableFor(sessions, 'DSH sessionQuery 需要 readSession');
159
+
160
+ const skills = service(ctx, 'skills');
161
+ if (skills && typeof skills.register === 'function') {
162
+ ports.skills = Object.freeze({ register: (definition) => registerWithEffect(ctx, skills, definition) });
163
+ } else unavailable.skills = unavailableFor(skills, 'DSH Skill registry 需要 register');
164
+
165
+ const llm = service(ctx, 'llm');
166
+ const defaults = service(ctx, 'agentDefaultModel');
167
+ if (llm && typeof llm.stream === 'function') {
168
+ ports.textModel = Object.freeze({
169
+ stream: (request) => llm.stream(request),
170
+ resolveModelInfo: typeof llm.resolveModelInfo === 'function'
171
+ ? (provider, model, signal) => llm.resolveModelInfo(provider, model, signal)
172
+ : undefined,
173
+ currentSelection: defaults && typeof defaults.currentSelection === 'function'
174
+ ? () => defaults.currentSelection()
175
+ : () => null
176
+ });
177
+ ports.visionModel = dshVisionPort(llm);
178
+ } else {
179
+ unavailable.textModel = unavailableFor(llm, 'DSH llm 需要 stream');
180
+ unavailable.visionModel = unavailableFor(llm, 'DSH llm 需要 stream');
181
+ }
182
+
183
+ const tools = service(ctx, 'tools');
184
+ if (tools && typeof tools.register === 'function') {
185
+ ports.tools = Object.freeze({ register: (definition) => registerWithEffect(ctx, tools, definition) });
186
+ } else unavailable.tools = unavailableFor(tools, 'DSH tools 需要 register');
187
+
188
+ return defineHostAdapter({ id: 'deepseek-harness', version, ports, unavailable });
189
+ }
@@ -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
+ }